题目
洛谷——P3957 [NOIP2017 普及组] 跳房子
ACwing——472. 跳房子
题意:
花最少的金币,获得分数k。
花g个金币可以升级机器人,使其跳跃距离范围向上向下增加g。
求满足分数下,最少使用金币数。
题解
方法一:贪心直接剪枝。
博客跳转:[题] 跳房子 #dp #二分答案
方法二:单调队列优化。
注意事项:
- 首先点名卡了我一天多的错误的点:单调队列出入队操作的先后顺序。
在这道题里面一定是先入队再出队。因为先出队的话,后续可能会让不在范围内的点进队没被出队,甚至直接排到队头影响当前点的答案,结果就错了。(例子没想到,举不出来……)
反过来,先把能入队的先入了,然后一起筛出队伍,保证队头一定是在范围里面的就不会出错了。- 范围。这道题之所以难,有一点就在范围上面,因为二分和单调队列都是对范围要求极其精准的算法,稍有不慎就会WA掉……
代码
#include <bits/stdc++.h>
//很大的数,初始化加个负号就当是负无穷了
#define INF 0x7f7f7f7f
using namespace std;
typedef long long LL;
const int N = 500010;
int n, d, k, L = 0, R = 20000, ans = -1, mid;
//R的大小可以通过x/n来确定,平均距离为2000
//f[i]是指到i点能获得的最高分。
//q是单调队列。
LL dist[N], w[N], f[N], q[N];
//检查花费g个金币进行改造后,最高得分是否会超过k。
bool check(int gold) {
memset(q, 0, sizeof q);
f[0] = 0;
//初始化为负无穷,因为格子上的分数有负数(神奇吧?)。
for(int i = 1; i <= n; i ++)
f[i] = -INF;
//机器人跳跃的弹性范围
int minn = max(1, d - gold), maxx = d + gold;
//st是头,ed是尾, nxt是下一个入队的元素。
//注意nxt是从起点开始的
int st = 0, ed = -1, nxt = 0;
//目的格子
for(int i = 1; i <= n; i ++) {
//维护区间[lf,rt]
int rt = dist[i] - minn, lf = dist[i] - maxx;
//这一步可以用单调队列维护
//区间为lf~rt
//先入队:对在不超过范围右端的元素进行入队
while(dist[nxt] <= rt) {
//注意该元素的f值不可为负无穷,这点的f值是负无穷的话
//说明前面的点不能跳到这点,自然就不能从这点跳到后面的点。
if(f[nxt] > -INF) {
//单调性不满足且队伍里有元素。
while(f[nxt] >= f[q[ed]] && ed >= st)
//队尾出队
ed --;
//从队尾加入新元素。
q[++ ed] = nxt;
}
nxt ++;
}
//再出队:对超出范围左端的元素进行出队。
while(dist[q[st]] < lf && st <= ed)
st ++;
if(st <= ed)
f[i] = w[i] + f[q[st]];
//完成目标。
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]);
while(L < R) {
//所有满足条件的情况都在mid的右边区间,
//搜索左边区间最小值
mid = L + R >> 1;
if(check(mid)) {
ans = mid;
R = mid;
}
else L = mid + 1;
}
cout << ans;
return 0;
}