题解:P3957 [NOIP2017 普及组] 跳房子
题目传送门。
老师、学长在讲 DP 的优化,同时我觉得题解区一些题解讲的不太清楚,因此来说一下个人的理解。
如有不足,欢迎指出。
什么是动态规划
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。划分出的每个子问题,通过一个公式,也就是状态转移方程联系起来,最后得到全局最优解。
题目分析
基本思路
根据题目的描述,发现很难用贪心与搜索实现,考虑用动态规划来实现,标签都说了。
一开始做的时候,对于金币的使用,我很是困惑,但是仔细想金币的作用,无非是扩大了跳跃的范围使机器人能够达到的范围更广,而求最小金币无非是枚举范围内的金币,看何时用最小金币满足条件(不一定要得到最高分数)。
暴力
虽然可以直接讲正解,但是还是讲讲暴力的做法来方便理解一下学长也是先让我们打暴力。
既然已经知道这是一道动态规划题目,写动态规划类的题目关键是设出状态,求出状态转移方程。
设
状态设完,就是求出状态转移方程了,金币如何并不会直接影响到 break;。
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+10;
ll n,d,k,l=0,r=N,mid;
//假设当前格子为i
ll f[N],x[N],s[N];//跳到第i个点的最大分数 第i个格子的距离 第i个格子的分数
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
bool find(int g){//查看金币是否能满足条件
memset(f,-N,sizeof(f));//初始化 由于可以为负数
f[0]=0;//一开始在0点
for(int i=1;i<=n;i++){
for(int j=i-1;j>=0;j--){
if(x[i]-x[j]>d+g){//如果超出范围
break;//停止循环
}//优化
if(x[i]-x[j]<max(ll(1),(d-g))){//注意类型 不能直接写 1
continue;
}
f[i]=max(f[i],f[j]+s[i]);//状态转移方程
if(f[i]>=k){//满足条件
return true;
}
}
}
return false;
}
int main(){
ll maxn=0;
n=read(),d=read(),k=read();//快读输入
for(int i=1;i<=n;i++){
x[i]=read(),s[i]=read();
maxn=max(maxn,x[i]);
}
ll g=2;//金币数从0开始枚举
while(g+d<maxn){//范围内
if(find(g)){
cout<<g<<'\n';
return 0;
}
g++;
}
cout<<-1<<endl;
return 0;
}
暴力代码大概只能过一半测试点,剩下的都超时了。
优化
作为 T4 肯定不会那么容易让你做出来的。刚刚的代码大概是
那么就是考虑优化了,看动态规划部分似乎没有什么可以改的了,那枚举金币的呢?
可以发现,由于金币数量是有序的,那么即可用二分查找来解决,对范围内的金币数进行二分查找,假如已经满足,将缩小金币数范围,减小金币数量,继续查找。假如还不能满足,则缩小金币数范围,增大金币数量使能到达的格子更多。
附上 AC 代码 ↓。
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+10;
ll n,d,k,l=0,r=N,mid;
//假设当前格子为i
ll f[N],x[N],s[N];//跳到第i个点的最大分数 第i个格子的距离 第i个格子的分数
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
bool find(int g){//查看金币是否能满足条件
memset(f,-N,sizeof(f));//初始化 由于可以为负数
f[0]=0;//一开始在0点
for(int i=1;i<=n;i++){
for(int j=i-1;j>=0;j--){
if(x[i]-x[j]>d+g){//如果超出范围
break;//停止循环
}//优化
if(x[i]-x[j]<max(ll(1),(d-g))){//注意类型 不能直接写1 如果距离为负按1来算
continue;
}
f[i]=max(f[i],f[j]+s[i]);//状态转移方程
if(f[i]>=k){//满足条件
return true;
}
}
}
return false;
}
int main(){
n=read(),d=read(),k=read();//快读输入
for(int i=1;i<=n;i++){
x[i]=read(),s[i]=read();
}
while(l<r){//由于是单调的序列可以使用二分
mid=(l+r)>>1;//直接平均可能会溢出
if(find(mid)){//看是否每个硬币都能做到
r=mid;
}
else{//不能满足
l=mid+1;//用更多金币尝试
}
}
if(l==N){//无法满足分数
cout<<-1;
}
else{//满足输出长度
cout<<l;
}
return 0;
}