P3957 跳房子

时间:2022-08-03 05:45:42

题目描述

跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。

跳房子的游戏规则如下:

在地面上确定一个起点,然后在起点右侧画 n 个格子,这些格子都在同一条直线上。每个格子内有一个数字(整数),表示到达这个 格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:

玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。

现在小 R研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 dd。小 R希望改进他的机器人,如果他花 g个金币改进他的机器人,那么他的机器人灵活性就能增加 g ,但是需要注意的是,每 次弹跳的距离至少为 11 。具体而言,当 g<dg<d 时,他的机器人每次可以选择向右弹跳的距离为 d-g,d-g+1,d-g+2,…, d+g-2 , d+g-1 , d+g ;否则(当 g \geq dg≥d时),他的机器人每次可以选择向右弹跳的距离为 1 , 2 , 3 ,…, d+g-2 , d+g-1 , d+g。

现在小 R希望获得至少 k 分,请问他至少要花多少金币来改造他的机器人。

输入输出格式

输入格式:

第一行三个正整数 n , d , k ,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数。相邻两个数之间用一个空格隔开。

接下来 n 行,每行两个正整数 xi​,si​ ,分别表示起点到第 i 个格子的距离以及第 i 个格子的分数。两个数之间用一个空格隔开。保证 xi​ 按递增顺序输入。

输出格式:

共一行,一个整数,表示至少要花多少金币来改造他的机器人。若无论如何他都无法获得至少 k 分,输出 −1 。

输入输出样例

输入样例#1: 复制
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
输出样例#1: 复制
2
输入样例#2: 复制
7 4 20
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
输出样例#2: 复制
-1

说明

【输入输出样例 1 说明】 2个金币改进后, 小 R 的机器人依次选择的向右弹跳的距离分别为2, 3, 5, 3, 4,3 先后到达的位置分别为 2, 5, 10, 13, 17, 20, 对应1, 2, 3, 5, 6, 7这6 个格子。这些格子中的数字之和15 即为小 R 获得的分数。

输入输出样例 2 说明

由于样例中 7个格子组合的最大可能数字之和只有 18 ,无论如何都无法获得20分

数据规模与约定

本题共 10 组测试数据,每组数据 10 分。

对于全部的数据满足1 ≤ n ≤ 500000, 1 ≤ d ≤2000, 1 ≤ x_i, k ≤ 10^9, |s_i| < 10^5。

对于第 1, 2组测试数据, n ≤ 10;

对于第3, 4, 5 组测试数据, n ≤ 500

对于第6, 7, 86,7,8 组测试数据, d = 1

解析:

(题外话,我能说这个题我好长时间理解错误么,我将这道题理解成了玩家可以从源点向右跳,跳到任何一个坐标为整数的位置,而本题要求:玩家每次都必须跳到当前位置右侧的一个格子内

,而这些格子是提前给定的。记录一下我的错误程序,可以把它改成另一个题呢

//起点为,玩家只可以想右跳,跳到坐标轴的整数点位置,只有个别位置有分数;
#include<iostream>
using namespace std;
int a[]={},b[];
int bo;
int l,h,t;
int n,d,k,maxx;
void dfs(int x,int sc,int dist,int t){//sc为到达x获得的分数,dist为当前灵活度
if (sc>=k) {
bo=;/*cout<<sc<<" :";for(int i=0;i<=t;i++)cout<<b[i]<<" ";cout<<endl;*/return ; }
for(int i=l;i<=h;i++){
int xx=x+i;
if (xx<=maxx){b[t+]=xx;dfs(xx,sc+a[xx],dist,t+);}
}
}
int main(){
int s=, x;
cin>>n>>d>>k;
for(int i=;i<=n;i++){ cin>>x;
cin>>a[x];
s+=a[x];
}
maxx=x;
if (s<k) cout<<-;
else
for(int i=;i<=;i++){//i为灵活性
bo=;
if(d>i)l=d-i;else l=;
h=d+i;
dfs(,,i,);
if (bo) {cout<<i;break;}
}
return ;
}

解法1:深度优先搜索(过3个点)

#include<cstring>
#include<iostream>
using namespace std;
const int maxn=;
int a[maxn][]={},b[maxn],f[maxn];
int bo;
int l,h,t,ans;
int n,d,k,maxx;
void dfs(int x,int sc,int pos){//sc为到达pos获得的分数
if (sc>=k) { bo=;return ; }//得到解
if(bo||x>n) return ;//越界或者已经得到解
if (a[x+][]-pos>h) return ;//不能跳那么远
if(a[x+][]-pos<l) {dfs(x+,sc,pos);return ;}//不能跳那么近
if (f[x+]<sc+a[x+][]){
f[x+]=sc+a[x+][];
dfs(x+,sc+a[x+][],a[x+][]);//走x+1这个点
dfs(x+,sc,pos);//跳过点x+1
    }
else if(f[x+]<sc){ dfs(x+,sc,pos);return ;} } int main(){ int s=, x; cin>>n>>d>>k; for(int i=;i<=n;i++){ cin>>a[i][]>>a[i][]; if (a[i][]>)s+=a[i][]; } int lift=,right=a[n][],mid; ans=-;//变量定义的位置一定要准,ans不能放在下面的while循环里,否则会受最后结果的影响 if (s<k) cout<<-; else{ while (lift<=right){//等号必需存在,有时正解就是lift=right的时候 mid=(lift+right)>>; if(d>mid)l=d-mid;else l=; h=d+mid; memset(f,-,sizeof(f)); f[]=; bo=;//标志本次递归是否会有解,一定要放在while里噢! dfs(,,); if(!bo) lift=mid+; else {ans=mid;right=mid-;} } cout<<ans<<endl; } return ; }

解法2:二分+动态规划

二分很显然,花的钱越多跳到范围越广,更容易满足条件。
于是我们二分花多少钱,检验能否拿到k分。
f[i]表示跳到第i个格子的最高得分:f[i]=max(f[j])+a[i][2] (L<=a[i][1]-a[j][1]<=H)(如果直接使用动态规划时间复杂度是O(n2),能过5个点,其余超时)

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=;
int a[maxn][]={},b[maxn];
long long f[maxn];//a[i][1]是距离,a[i][2]是值。
int bo;
int l,h,t,ans;
int n,d,k,maxx,inf=-0X7f;
int dp(){
f[]=;//源点设置为0
for(int i=;i<=n;i++){
for(int j=;j<i;j++){
if(a[i][]-a[j][]>=l&&a[i][]-a[j][]<=h&&f[i]<f[j])//可以跳到
f[i]=f[j];
}
f[i]=f[i]+a[i][];
if(f[i]>=k) return ;
}
return ;
}
int main(){
int s=, x;
cin>>n>>d>>k;
for(int i=;i<=n;i++){
cin>>a[i][]>>a[i][];
if (a[i][]>)s+=a[i][];
}
int lift=,right,mid;
right=max(d,a[n][]);
ans=-;//变量定义的位置一定要准,ans不能放在下面的while循环里,否则会受最后结果的影响
if (s<k) cout<<-;
else{
while (lift<=right){//等号必需存在,有时正解就是lift=right的时候
mid=(lift+right)>>;
if(d>mid)l=d-mid;else l=;
h=d+mid;
memset(f,-,sizeof(f));
f[]=;
bo=;//标志本次递归是否会有解,一定要放在while里噢!
bo=dp();
// cout<<bo<<" "<<mid<<": ";
// for(int i=0;i<=n;i++)
// cout<<f[i]<<" ";
// cout<<endl;
if(!bo) lift=mid+;
else {ans=mid;right=mid-;}
}
cout<<ans<<endl;
// for(int i=0;i<n;i++)
// cout<<f[i]<<" ";
}
return ;
}

解法3:二分+动态规划+单调队列

这个题作为2017普及组的压轴题,各种坑。真正的考察大家的综合能力。

(1)数据类型的定义,求和的变量要定义为long long(因为:1≤xi​,k≤109)

(2)二分时右边界right=max(d,a[n][1]);//比如d=100,a[n][1]=10 ,坑啊,要考虑全面,加上这一条就ac,否则只过了5个点呢!!!

(3)单调队列的变形使用

考虑到f[i]总是从前面可跳区间的最大值跳过来,随着i往后面走,这个区间也往后面走,如果采用单调队列,速度会快很多,因为每个元素只进队出队一次,时间复杂度为O(n)。

但是我们发现了一个问题,对于某个决策k,它可能不能更新i,但是它能更新j(j<i)。这样就不能直接用根据大小或者无法更新当前决策把k出队。

换句话说:不能像普通的单调队列优化一样仅凭这个决策的值小于另一个决策的值就将决策排除候选集合,因为我们的可行决策区间,是从[L,H],一个决策的值很大,但是他不一定可以更新他的下一个格子,一些较劣决策仍然可能成为最优决策,这是我们最需要注意的。

这里我们巧妙的对单调队列进行了变形:

入队:求f[i]前,先考虑j是否入队(j<i),如果j到i的最短距离<l,则j不入队,因为他对求i没有帮助,否则将所有符合条件的j按单调队列的规则(小于j的值出队)入队。然后求f[i].

出队:如果队首到i的距离大于H,则出队。

//考虑到f[i]总是从前面可跳区间的最大值跳过来,随着i往后面走,这个区间也往后面走,如果采用单调队列,速度会快很多,但是需要用到双端队列来构造单调队列,代码相对复杂一点,
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=500010;
int a[maxn][3]={0};//a[i][1]是距离,a[i][2]是值。
long long f[maxn],q[maxn][3];
int bo;
int l,h,t,ans=-1;
int n,d,k,maxx;
long long s;
const long long inf=-0X8080808080808080;
void init(){
cin>>n>>d>>k;
for(int i=1;i<=n;i++){
cin>>a[i][1]>>a[i][2];
if (a[i][2]>0)s+=a[i][2];//记录所有的正整数之和
}
}
int dp(){
memset(f,0x80,sizeof(f));
f[0]=0;//源点设置为0
memset(q,0,sizeof(q));
int head=1,tail=0,j=0;//tail指向队尾的最后一个元素,head指向队首元素
for(int i=1;i<=n;i++){ //求出i后并不入队,因为
while(a[i][1]-a[j][1]>=l&&j<i){ //巧:如果j到i的最短距离<l,则j不入队,一直到满足条件为止。 ,>h的在后面删除
if(f[j]!=inf){
while(head<=tail&&q[tail][2]<f[j]) tail--;//队尾指针比j小
tail++;//j入队
q[tail][1]=a[j][1];
q[tail][2]=f[j];
}
j++;
}
while (a[i][1]-q[head][1]>h&&tail>=head)head++;//i到队首的距离>h,则从队首出队。
if(head<=tail){
f[i]=q[head][2]+a[i][2];
if(f[i]>=k) return 1;
}
}
return 0;
}
int main(){
init();
int lift=0,right,mid;
right=max(d,a[n][1]);//比如d=100,a[n][1]=10 ,坑啊,要考虑全面,加上这一条就ac,否则只过了5个点呢!!!
ans=-1;//变量定义的位置一定要准,ans不能放在下面的while循环里,否则会受最后结果的影响
if (s<k) cout<<-1;
else{
while (lift<=right){//等号必需存在,有时正解就是lift=right的时候
mid=(lift+right)>>1;
if(d>mid)l=d-mid;else l=1;
h=d+mid;
memset(f,-127,sizeof(f));
f[0]=0;
bo=0;//标志本次递归是否会有解,一定要放在while里噢!
bo=dp();
if(!bo) lift=mid+1;
else {ans=mid;right=mid-1;}
}
cout<<ans<<endl;
}
return 0;
}