题目描述
一个猴子找到了很多香蕉树,这些香蕉树都种在同一直线上,而猴子则在这排香蕉树的第一棵树上。这个猴子当然想吃尽量多的香蕉,但它又不想在地上走,而只想从一棵树跳到另一棵树上。同时猴子的体力也有限,它不能一次跳得太远或跳的次数太多。每当他跳到一棵树上,它就会把那棵树上的香蕉都吃了。那么它最多能吃多少个香蕉呢?
输入
输入第一行为三个整数,分别是香蕉树的棵数N,猴子每次跳跃的最大距离D,最多跳跃次数M。下面N行每行包含两个整数,ai,bi,分别表示每棵香蕉树上的香蕉数,以及这棵树到猴子所在树的距离。输入保证这些树按照从近到远排列,并且没有两棵树在同一位置。b0总是为0。
输出
输出只有一行,包含一个整数,为猴子最多能吃到的香蕉数
样例输入
5 5 2
6 0
8 3
4 5
6 7
9 10
样例输出
20
提示
ai<=10000,bi<=10000,D<=10000 ;
30%的数据有M<N<=100;
50%的数据有M<N<=2000;
100%的数据有M<N<=5000。
思路
当我们拿到一道题时,首先按照日常的思维来思考最暴力的解决方法。如下图所示,如果把所有可能的方案列出来,那就是从某个点i出发,一步走一格,一步走两格,一步走三格(蓝色线)...从点i+1出发也是如此,一步走一格,一步走两格(红色线)...在所有可能的路线中把对应的香蕉数量加起来,取其中的最大值。
暴力求解(搜索)
使用搜索来模拟上述的过程的话,搜索函数的状态设计应当包括下面的关键参数:
1、当前所处位置
2、跳到当前位置已经花费的次数
3、跳到当前位置已有的苹果数量
可设搜索的状态为S(i,j,num)
知道如何使用状态来描述问题了,那么写搜索函数就很简单了。
【代码参考】
//搜索
#include <iostream>
using namespace std;
int ans;
const int MAXN=5019;
const int MAXM=5019;
int a[MAXN],b[MAXN];
int n,m,d;
void dfs(int x,int y,int num){
if(y>m) return ;
for(int i=x+1;i<=n;i++)
if(b[i]-b[x]<=d){
ans=max(ans,num+a[i]);
dfs(i,y+1,num+a[i]);
}
else break;//剪枝:如果当前位置已经跳不过去了,后面的位置肯定也跳不过去
}
int main(){
scanf("%d%d%d",&n,&d,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i],&b[i]);
ans=a[1];
dfs(1,1,a[1]);
printf("%d",ans);
return 0;
}
DP求解
如果仅仅使用搜索不加其它优化的话,还是会有多个数据点TL,这时候我们发现很多子问题重复求解了,使用表把子问题的解来保存下来。
设f[i,j]为当前位置处于i并且已经走了j步的最优解,
状态转移方程:f[i,j]=max{ f[k,j-1]}+a[i](0<=k<i &&b[i])-b[k]<=D)
【代码参考】
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN=5019;
const int MAXM=5019;
int f[MAXN][MAXM];
int a[MAXN],b[MAXN];
int n,m,d;
int ans=0;
int main(){
scanf("%d%d%d",&n,&d,&m);
memset(f,-0x3f,sizeof f); /*此行必不可少,原因是b[i]-b[k]<=d如果成立,
原因是如果k点不可以到达,将k点初始化为0,会对正确的结果产生影响*/
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i],&b[i]);
ans=f[1][0]=a[1];
for(int j=1;j<=m;j++)
for(int i=2;i<=n;i++)
{
for(int k=i-1;k>=1;k--)
{
if(b[i]-b[k]<=d)
f[i][j]=max(f[i][j],f[k][j-1]+a[i]);
else break;
}
ans=max(ans,f[i][j]);
}
printf("%d",ans);
return 0;
}
需要注意的是f数组需要初始化为一个负无穷的数,原因是如果点K不可到达且b[i]-b[k]成立的话,可能对正确的结果产生影响。
DP求解(优先队列优化)
仔细观察状态转移方程:f[i,j]=max{ f[k,j-1]}+a[i](0<=k<i &&b[i])-b[k]<=D),我们发现是求[0,k-1]这个区间的最优解,可以使用优先队列来维护这个区间的最值。
当从i位置转移到i+1位置时,也是就是求f[i,j+1]时,当前需要求解最优解的区间变成了[0,k],需要将新的状态f[i-1,j-1]加入到到优先队列中。
队首元素可能不是一个正确的最优解,所以在获取队首元素时,判断一下新位置能不能到达队首元素所处的位置,如果不能到达,则删除队首元素。
【代码参考】
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
struct data{
int pos;
int val;
bool operator <(const data &a)const{
return a.val>val;
}
data(int _pos,int _val):pos(_pos),val(_val){}
};
const int MAXN=5019;
const int MAXM=5019;
int f[MAXN][MAXM];
int a[MAXN],b[MAXN];
int n,m,d;
int ans=0;
int main(){
scanf("%d%d%d",&n,&d,&m);
memset(f,-0x3f,sizeof f);
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i],&b[i]);
ans=f[1][0]=a[1];
for(int j=1;j<=m;j++){
priority_queue<data> q;
for(int i=2;i<=n;i++){
q.push( data(b[i-1],f[i-1][j-1]));
while(b[i]-q.top().pos>d&&!q.empty()) q.pop();//删除不符合要求的队首元素
if(q.size()==0) break; //如果队列为空,则说明当前情况下也不可能再往后面跳了
f[i][j]=max(f[i][j],q.top().val+a[i]);
ans=max(ans,f[i][j]);
}
}
printf("%d",ans);
return 0;
}
DP求解(单调队列优化)
使用优先队列优化dp时,队列里面保存了很多无用的状态,我们可以使用单调队列来优化,在插入新元素的同时删除掉永远不可能获得正确解的状态。
观察状态转移方程:f[i,j]=max{ f[k,j-1]}+a[i](0<=k<i &&b[i])-b[k]<=D),随着i的增加,决策的区间是单调的,所以在计算f[i,j]的值是,可以维护一个单调队列。
当计算f[i,j]时,把新决策的值f[i-1,j-1]和一一和队尾元素进行比较,如果比队尾元素大,显然队尾元素没有再保留的必要了,删除队尾元素,否则就插入到队尾元素的后面。
在计算f[i,j]时直接取队首元素,与优先队列优化的方式一样,在获取队首元素时,判断一下i能不能到达队首元素所处的位置,如果不能到达,则删除队首元素,直到能到达为止。
【参考代码】
//使用单调队列优化
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXN=5019;
const int MAXM=5019;
int f[MAXN][MAXM];
int a[MAXN],b[MAXN];
int q[MAXN]={0},pos[MAXN]={0};
int n,m,d;
int ans=0;
int main(){
scanf("%d%d%d",&n,&d,&m);
memset(f,-0x3f,sizeof (f));
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i],&b[i]);
ans=f[1][0]=a[1];
for(int j=1;j<=m;j++){
int head=1,tail=0;
memset(q,-0x3f,sizeof(q));
memset(pos,0x3f,sizeof(pos));
for(int i=2;i<=n;i++){
while(f[i-1][j-1]>=q[tail]&&head<=tail) tail--;
while(b[i]-pos[head]>d&&head<=tail) head++;
q[++tail]=f[i-1][j-1];
pos[tail]=b[i-1];
if(b[i]-pos[head]<=d) //处理特殊情况,当队列中只有一个元素时,也可能是不符要求的
f[i][j]=max(f[i][j],q[head]+a[i]);
ans=max(ans,f[i][j]);
}
}
printf("%d",ans);
return 0;
}
总结
做题时我们可以先根据题意大概画一下模型,通过模型来思考使用最”暴力“的方法如何解决问题,通过暴力的方法,也就知道了在某个场景下,有多少种不同的情况需要处理。
之后就可以使用抽象语言来描述问题了,如使用搜索算法求解本题时用状态S(i,j,num)来描述问题。
如果题目还满足动态规划的要求:1.最优子结构,2.无后效性。那么就可以用表来保存子问题,这样就不需要重复求解大量的子问题。
所谓的动态规划,本质上还是“暴力”,与搜索不同的就是动态规划把所有可能的情况的解保存起来。在求解这类问题时,如果知道搜索怎么做,那么基本上就知道使用动态规则怎么做了。
如何判断dp方程可不可以使用单调队列优化?
通过观察DP方程,如果是求区间最值且可选策略(本题里面的策略变量为j)是单调的,那么可以考虑使用优先队列或单调队列优化。