[NOIP2017 普及组] 跳房子
题目背景
NOIP2017 普及组 T4
题目描述
跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。
跳房子的游戏规则如下:
在地面上确定一个起点,然后在起点右侧画 n n n 个格子,这些格子都在同一条直线上。每个格子内有一个数字(整数),表示到达这个 格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:
玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。
现在小 R 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 d d d。小 R 希望改进他的机器人,如果他花 g g g 个金币改进他的机器人,那么他的机器人灵活性就能增加 g g g,但是需要注意的是,每 次弹跳的距离至少为 1 1 1。具体而言,当 g < d g<d g<d 时,他的机器人每次可以选择向右弹跳的距离为 d − g , d − g + 1 , d − g + 2 , … , d + g − 1 , d + g d-g,d-g+1,d-g+2,\ldots,d+g-1,d+g d−g,d−g+1,d−g+2,…,d+g−1,d+g;否则当 g ≥ d g \geq d g≥d 时,他的机器人每次可以选择向右弹跳的距离为 1 , 2 , 3 , … , d + g − 1 , d + g 1,2,3,\ldots,d+g-1,d+g 1,2,3,…,d+g−1,d+g。
现在小 R 希望获得至少 k k k 分,请问他至少要花多少金币来改造他的机器人。
输入格式
第一行三个正整数 n , d , k n,d,k n,d,k,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数。相邻两个数 之间用一个空格隔开。
接下来 n n n 行,每行两个整数 x i , s i x_i,s_i xi,si,分别表示起点到第 i i i 个格子的距离以及第 i i i 个格子的分数。两个数之间用一个空格隔开。保证 x i x_i xi 按递增顺序输入。
输出格式
共一行,一个整数,表示至少要花多少金币来改造他的机器人。若无论如何他都无法获得至少 k k k 分,输出 − 1 -1 −1。
样例 #1
样例输入 #1
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
样例输出 #1
2
样例 #2
样例输入 #2
7 4 20
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
样例输出 #2
-1
提示
样例 1 说明
花费 2 2 2 个金币改进后,小 R 的机器人依次选择的向右弹跳的距离分别为 $ 2, 3, 5, 3, 4,3$,先后到达的位置分别为 2 , 5 , 10 , 13 , 17 , 20 2, 5, 10, 13, 17, 20 2,5,10,13,17,20,对应 $ 1, 2, 3, 5, 6, 7$ 这 6 6 6 个格子。这些格子中的数字之和 $ 15$ 即为小 R 获得的分数。
样例 2 说明
由于样例中 7 7 7 个格子组合的最大可能数字之和只有 18 18 18,所以无论如何都无法获得 20 20 20 分。
数据规模与约定
本题共 10 组测试数据,每组数据等分。
对于全部的数据满足 1 ≤ n ≤ 5 × 1 0 5 1 \le n \le 5\times10^5 1≤n≤5×105, 1 ≤ d ≤ 2 × 1 0 3 1 \le d \le2\times10^3 1≤d≤2×103, 1 ≤ x i , k ≤ 1 0 9 1 \le x_i, k \le 10^9 1≤xi,k≤109, ∣ s i ∣ < 1 0 5 |s_i| < 10^5 ∣si∣<105。
对于第 1 , 2 1, 2 1,2 组测试数据,保证 n ≤ 10 n\le 10 n≤10。
对于第 3 , 4 , 5 3, 4, 5 3,4,5 组测试数据,保证 n ≤ 500 n \le 500 n≤500。
对于第 6 , 7 , 8 6, 7, 8 6,7,8 组测试数据,保证 d = 1 d = 1 d=1。
思路
- 对于这道题,我们首先肯定会想到用二分,就是给定一个金币数,看看能不能使得小 R 获得至少 k k k 分。这样就产生了关系。
- 那么我们写着写着,发现:由于小 R 在比赛过程中是可以任意时刻退出比赛的,因此如果我们贪心去做的话,肯能会出错,因为他也可以任意时刻进场。因此我们使用dp。
- 那么此时的dp的状态表示为: f i f_i fi 表示所有以 i i i 结尾的最高分数的集合。
- 状态转移:
f
i
=
m
a
x
(
f
i
,
f
k
+
w
i
)
,
0
≤
k
<
i
f_i=max(f_i,f_k+w_i),0\le k<i
fi=max(fi,fk+wi),0≤k<i。注意,因为本道题可能有负数,所以我们的
f
f
f 值要
memset
成-0x3f
。 - 那么我们此时能写出:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
#define int long long
using namespace std;
const int N = 5e5+10;
int x[N],w[N];
int n,d,k;
int ans;
int maxv;
int f[N];
bool check(int mid){
int l;
memset(f,-0x3f,sizeof f);
if(mid<d){
l=max(1ll,d-mid);
}else{
l=1;
}
//超时
f[0]=0ll;
for(int i=1;i<=n;i++){
for(int j=i-1;j>=0;j--){
if(x[i]-x[j]>mid+d)break;
if(x[i]-x[j]<l)continue;
f[i]=max(f[i],f[j]+w[i]);
}
if(f[i]>=k)return true;
}
return false;
}
signed main(){
cin>>n>>d>>k;
for(int i=1;i<=n;i++){
cin>>x[i]>>w[i];
if(w[i]>0){
ans+=w[i];
}
}
if(ans<k){
puts("-1");
}else{
int l=-1,r=x[n]-x[1]+1;
while(l+1!=r){
int mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid;
}
cout<<r;
}
return 0;
}
但不管你在输入输出优化,发现只能得 80 p t s 80\ pts 80 pts,因为时间复杂度接近 O ( n 2 ) O(n^2) O(n2)。
-
因此我们得优化。
-
观察上面 dp 转移式中 k k k 需满足的条件,会发现,显然对于每一个 i i i, k k k 的取值范围都是一个区间。有着丰富经验的选手应该可以一眼看出,随着 i i i 不断变大,这个区间的 ( l , r ) (l,r) (l,r) 也是不断递增的,于是我们就可以用单调队列来优化这个 dp 了。
-
优化后每次 dp 的时间复杂度为 O ( n ) O(n) O(n),加上二分的复杂度,总时间复杂度为 O ( n log x n ) O(n\log x_n) O(nlogxn),足以通过此题。
-
对于不满足题意的就是把正数加起来小于 p p p 就输出
-1
。 -
细节1:最好开
long long
。 -
细节2:我的代码是用双端队列来优化的,思路跟单调队列优化类似,都是区间优化类的。
AC 代码
#include<iostream>
#include<algorithm>
#include<cstring>
#include<deque>
#define int long long
using namespace std;
const int N = 5e5+10;
int x[N],w[N];
int n,d,k;
int ans;
int maxv;
int f[N];
int q[N];
bool check(int mid){
int l;
memset(f,-0x3f,sizeof f);
if(mid<d){
l=max(1ll,d-mid);
}else{
l=1;
}
//超时
f[0]=0ll;
// for(int i=1;i<=n;i++){
// for(int j=i-1;j>=0;j--){
// if(x[i]-x[j]>mid+d)break;
// if(x[i]-x[j]<l)continue;
// f[i]=max(f[i],f[j]+w[i]);
// }
// if(f[i]>=k)return true;
// }
deque<int>q;//单调队列
int now=0;
for(int i=1;i<=n;i++){
while(x[now]+l<=x[i]){//在i之前找到满足能跳到i的,把它们入列
while(q.size()&&f[q.back()]<f[now]){//列内的尾巴的价值不行,就把它弹走,把那个now加入
//这里就是单调队列的求最大值的模板
q.pop_back();
}
q.push_back(now);
now++;
}
while(q.size()&&x[q.front()]+mid+d<x[i]){
//如果列的前端元素的坐标+最大能跳的距离小于当前坐标,不能要了
q.pop_front();
}
if(q.size()){//此时更新
f[i]=f[q.front()]+w[i];
}
if(f[i]>=k)return true;//随时return
}
return false;
}
signed main(){
scanf("%lld%lld%lld",&n,&d,&k);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&x[i],&w[i]);
if(w[i]>0){
ans+=w[i];
}
}
if(ans<k){
puts("-1");
}else{
int l=-1,r=x[n]-x[1]+1;
while(l+1!=r){
int mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid;
}
printf("%lld",r);
}
return 0;
}