GDKOI2009-猴子 Monkey

题目描述

一个猴子找到了很多香蕉树,这些香蕉树都种在同一直线上,而猴子则在这排香蕉树的第一棵树上。这个猴子当然想吃尽量多的香蕉,但它又不想在地上走,而只想从一棵树跳到另一棵树上。同时猴子的体力也有限,它不能一次跳得太远或跳的次数太多。每当他跳到一棵树上,它就会把那棵树上的香蕉都吃了。那么它最多能吃多少个香蕉呢?

输入

输入第一行为三个整数,分别是香蕉树的棵数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)是单调的,那么可以考虑使用优先队列或单调队列优化。

猜你喜欢

转载自blog.csdn.net/Zerotogether/article/details/90409781