题目描述
跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。
跳房子的游戏规则如下:
在地面上确定一个起点,然后在起点右侧画n个格子,这些格子都在同一条直线上。
每个格子内有一个数字(整数),表示到达这个格子能得到的分数。
玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。
第二次再从当前位置继续向右跳,依此类推。
规则规定:玩家每次都必须跳到当前位置右侧的一个格子内。
玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。
现在小R研发了一款弹跳机器人来参加这个游戏。
但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的d。
小R希望改进他的机器人,如果他花g个金币改进他的机器人,那么他的机器人灵活性就能增加g,但是需要注意的是,每次弹跳的距离至少为1。
具体而言,当g<d时,他的机器人每次可以选择向右弹跳的距离为d-g, d-g+1, d-g+2,…,d+g-2,d+g-1,d+g;否则(当g≥d时),他的机器人每次可以选择向右弹跳的距离为1,2,3,…,d+g-2,d+g-1,d+g。
现在小R希望获得至少k分,请问他至少要花多少金币来改造他的机器人。
输入格式
第一行三个正整数n,d,k,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数,相邻两个数之间用一个空格隔开。
接下来n行,每行两个正整数$x_i,s_i$,分别表示起点到第$i$个格子的距离以及第$i$个格子的分数。
两个数之间用一个空格隔开,保证$x_i$按递增顺序输入。
输出格式
共一行,一个整数,表示至少要花多少金币来改造他的机器人。
若无论如何他都无法获得至少k分,输出-1。
样例
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
A-G分别表示1号到7号格子,红色文字表示该格子对应的分值。
初始状态,当d = 4
时,无法由起点跳到其它点,此时需要花费2
金币改造,改造后机器人的移动范围变为$[2,6]$,此时:
1. 机器人跳到A
点,得6分,总分6分
2. 机器人跳到B
点,得-3分,总分3分
3. 机器人跳到C
点,得3分,总分6分
4. 机器人跳到E
点,得1分,总分7分
5. 机器人跳到F
点,得6分,总分13分
所以,当花费2
金币进行改造时,得分不低于10分。
算法思想(二分搜索+动态规划)
- 假设已求得了最优解,即花费$g$个金币进行改造后,得分不低于$k$。如果在此状态下再花费若干金币,那么得分一定不低于$k$。因此花费的金币数和得分之间满足单调性质,是一个单调递增关系(不一定是严格单调递增),可以使用二分求解在花费不同金币进行改造时得分是否满足条件。
- 要求花费$g$个金币进行改造后的最高得分,可以使用动态规划的思想:
- 状态表示:
f[i]
表示在花费g
个金币进行改造后,跳过前i
个格子得到的最高得分 - 状态转移:
f[i] = max{f[j] + w[i]}
,其中$\max(1, d-g)\le dist[i]-dist[j]\le d+g$
- 状态表示:
时间复杂度
$O(n\times d\times log\frac{x}{n})$,需要对DP进行剪枝。
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 500010;
int n, d, k;
//五年OI一场空,不开long long见祖宗
LL dist[N], w[N], f[N];
//检查花费g个金币进行改造后,最高得分是否会超过k
bool check(int gold)
{
//机器人能够弹跳的范围[L,R]
int L = max(1, d - gold);
int R = d + gold;
//注意:这里要初始化为负无穷
memset(f, 0xc0, sizeof f);
f[0] = 0;
for(int i = 1; i <= n; i ++)
{
//从i的前一个格子开始,枚举到起点
for(int j = i - 1; j >= 0; j --)
{
//剪枝,机器人从j号格子加上R还是法到达i号格子
//那么从j号格子之前的格子也无法到达i号格子
if(dist[j] + R < dist[i]) break;
//机器人从j号格子加上L还是超过了i号格子
//那么继续尝试j号之前的格子
if(dist[j] + L > dist[i]) continue;
//机器人从j号格子可以转移到i号格子
f[i] = max(f[i], f[j] + w[i]);
if(f[i] >= k) return true;
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n, &d, &k);
for(int i = 1; i <= n; i ++)
scanf("%lld%lld", &dist[i], &w[i]);
//R的大小可以通过x/n来确定,平均距离为2000
int L = 0, R = 20000, ans = -1;
while(L < R)
{
//所有满足条件的情况都在mid的右边区间,
//搜索右边区间最小值
//适用二分搜索模板1
int mid = L + R >> 1;
if(check(mid))
{
ans = mid;
R = mid;
}
else L = mid + 1;
}
cout << ans << endl;
return 0;
}
二分的 R=2w 是怎么确定的?
谢谢你的代码思路,感觉y总单调队列优化不是很好写。。。。
楼主,for(int j = i - 1; j >= 0; j –)这个枚举太费时间了,其实可以不用枚举到[i-1,0],改成for(int j=i-1;j>=i-r;j–)这样就可以过了
我读小学生6年级,希望大神能帮我解惑
这里从
i
的前一个格子开始,枚举到起点0
,是为了方便剪枝。如果从前向后枚举,剪枝的效率不高,最终TLE。另外,从前向后枚举需要改一下剪枝的条件,才会避免WA,但还是会TLE:非常感谢
大神,check函数里j循环为什么要从i-1到0,从0到i-1我试了是不对的,不知为什么