题解 P3957 【跳房子】
Gorenstein · · 题解
原题链接:P3957 跳房子
题目大意
- 直线的正方向上有
n 个点,他们到原点的距离为x_i ,第i 个点有一个权值s_i 。权值可能为负。 - 从原点开始向右跳。假设原点为第
0 个点,每次能跳到\left[s_i+d-g,s_i+d+g\right] 中在第i 个点右边的任意一个点,并获得对应的权值。 - 给定
d,k ,现在想要在获得的权值之和大于等于k 的前提下,最小化g 。 -
分析
容易发现,花费的金币越多,则跳跃距离的灵活度也更大。故而当金币增加时,最大得分是不降的。因此最大得分对金币数具有单调性,故二分判定
//二分判定mid(假设单调上升)
l=0,r=100005;
while(l<r){
long long mid=l+r>>1;
if(check(mid))r=mid;
else l=mid+1;
}
然后思考如何实现判定。容易发现这是一个基本的线性 DP 问题。
每一个点都需要从它左边的点跳过来,并且后面的跳法不受前面如何条约影响。同时,一个点的最大得分,是从能跳到这个点的前面所有点中选取最优的一个。
我们设
若发现一个 true,否则判定失败。时间复杂度
//38 lines of AC_code
long long n,d,k,f[500005],a[500005][2];
bool check(int g){
memset(f,-127,sizeof(f));
long long x=max(d-g,(long long)1),s=d+g;
f[0]=0;
//外层循环枚举阶段i
for(long long i=1;i<=n;i++){
//内层循环对决策点j进行转移
for(long long j=i-1;j>=0;j--){
if(a[i][0]-a[j][0]<x)continue;
if(a[i][0]-a[j][0]>s)break;
//实行状态转移方程
f[i]=max(f[i],f[j]+a[i][1]);
if(f[i]>=k)return true;
}
}
return false;
}
int main(){
cin>>n>>d>>k;
for(long long i=1;i<=n;i++)
cin>>a[i][0]>>a[i][1];
l=0,r=100005;
//二分判定花费mid是否可行
while(l<r){
long long mid=l+r>>1;
if(check(mid))r=mid;
else l=mid+1;
}
if(l==100005)cout<<-1;
else cout<<l;
return 0;
}
单调队列优化
我们还能继续进行优化。
继续思考,容易发现,当
单调队列中的元素维护的值满足单调性,并且每个元素对应在原序列中的顺序必须单调递增。每一次区间的移动,队首就是最优解。
建立单调队列
- 对于每个阶段
i ,将决策候选集合中的元素j 从队尾插入单调队列。此题中是区间最大值,所以队首应为未过时的所有决策中最大值。内部决策单调递减; - 检查队首,排除过时决策;
- 直接取队首作为当前状态的最优决策,实行转移;
- 转移过后,判断分数是否已经大于
k ,如果是,返回判定成功。
所有
//38 lines of AC_code
long long n,d,k,f[500005],a[500005][2],q[500005],l=0,r=100005;
bool check(int g){
memset(f,-,sizeof(f));
memset(q,0,sizeof(q));
long long x=max(d-g,(long long)1),s=d+g;
long long head=1,tail=0,j=0;
f[0]=0;
//依次转移每个i
for(long long i=1;i<=n;i++){
//决策候选集合
while(a[i][0]-a[j][0]>=x&&j<i){
if(f[j]>-99999999){
//检查队尾单调性
while(f[q[tail]]<=f[j]&&head<=tail)tail--;
q[++tail]=j;//符合单调性后,插入决策j
}
j++;
}
//检查队头是否过时
while(a[i][0]-a[q[head]][0]>s&&head<=tail)head++;
if(head<=tail)f[i]=f[q[head]]+a[i][1];//实行决策
if(f[i]>=k)return true;
}
return false;
}
int main(){
cin>>n>>d>>k;
for(long long i=1;i<=n;i++)cin>>a[i][0]>>a[i][1];
//二分判定花费mid个金币是否可行
while(l<r){
long long mid=l+r>>1;
if(check(mid))r=mid;
else l=mid+1;
}
if(l==100005)cout<<-1;
else cout<<l;
return 0;
}