【蓝桥杯】历届试题 地宫取宝(记忆化搜索、dfs、dp)—— 酱懵静

历届试题 地宫取宝

问题描述
  X 国王有一个地宫宝库。是 n x m 个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。
  地宫的入口在左上角,出口在右下角。
  小明被带到地宫的入口,国王要求他只能向右或向下行走。
  走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
  当小明走到出口时,如果他手中的宝贝恰好是k件,则这些宝贝就可以送给小明。
  请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这k件宝贝。

输入格式
  输入一行3个整数,用空格分开:n m k (1<=n,m<=50, 1<=k<=12)
接下来有 n 行数据,每行有 m 个整数 Ci (0<=Ci<=12)代表这个格子上的宝物的价值

输出格式
要求输出一个整数,表示正好取k个宝贝的行动方案数。该数字可能很大,输出它对 1000000007 取模的结果。

样例输入
2 2 2
1 2
2 1
样例输出
2

样例输入
2 3 2
1 2 3
2 1 5
样例输出
14



——分割线之初入江湖——



分析:
这道题给人的第一感觉是要用dfs,毕竟这种走迷宫的模型真的都快被dfs和bfs玩坏了
于是开始往那个方向想
首先最容易想到的解决方案是:我直接从起点dfs到终点,用一个向量vector来保存这之间走过的每个点上的宝物价值,每当我到达终点(即找到了一条从起点到终点的行走方案),就调用另一个函数来求出这个向量中满足从小到大递增序列的所有情况的数量。这样一来,当dfs结束也就得到了想要的答案。

上面的分析是一个几乎人人都能看懂的算法思路,浅显易懂是他的优点,而耗时冗余则是他的缺点。当然了,对于本题而言,这个缺点的直接影响就是你无法得到满分。你可以想想,本来递归就那么多层了,还要在每一层结束的地方去利用动态规划来求出递增序列的个数,啧啧啧~~~超时无疑。

于是乎我开始想,可不可以在递归过程中顺便把递增序列也给找了?
当然是可以的,我的主要思路是在dfs函数里面多加两个参数,一个用于标记当前小明手上所有宝物的最大值maxValue,另一个用于表示当前小明手上的宝物的数量num。每当你进入一个dfs时,都可以判断当前这个宝物拿还是不拿,从而进入下一层dfs。然后当最后到了终点的位置时,根据当前手上的宝物数量num的值来判断当前这种取法是否合理。合理的要求当然是两种:
1.num == k(即手上的宝物数量刚刚为k)
2.num == k-1 && maxValue<map[x][y](即手上的宝物数量为k-1,但是最后一个宝物的价值大于手上最大的)
最后dfs完毕,也就求出了答案

关于小明手上宝物的最大值,最开始我的想法很单纯,就是每当你要拿某个宝物的时候,就把这个宝物的价值压栈进一个vector向量v,然后每当你需要求最大值的宝物时,就去遍历这个向量然后返回最大值(简直不要太喜欢向量了)。
可是后来我仔细想了下,“走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它”,这句话的意思不是暗示了,每当你拿起一个宝物时,这个宝物就一定是你当前所拿的所有宝物中价值中最大的那个么?因此,“求宝物的最大值”实际上就成为了“传递宝物的最大值”,所以只需要在dfs中多加一个用于标记当前小明手上所有宝物中的最大值maxValue即可。

当有了这样的一种思路后,我们的dfs可以用下面的一个公式来描述:
dfs的组成
他的意义是,当你进入某个点(坐标为(x,y))时,在接下来进入另一个点前会有以下两个大类情况:
1.如果当前位置上的宝物的价值(map[x][y])大于目前手上任何一个宝物的价值,那么这个宝物是可以拿的(当然,你也可以不拿)。用代码表示当前状态(还未到下一个位置)应该为dp[x][y][num+1][map[x][y]](拿了)或者dp[x][y][num][maxValue](不拿)。可是在进入下一个点时,又有两种情况:向下走或者向右走。用代码表示就是dp[x+1][y](向下走)或dp[x][y+1](向右走)。综合这两个情况,就会有2╳2=4种情况,也就是上面的公式所描述的第一种情况(前提是map[x][y]>max),它涵盖的意思是,对于每一个满足map[x][y]>maxValue的点,以它为起点进行dfs都会有四条路。
2.如果当前位置上的宝物的价值(map[x][y])不大于目前手上任何一个宝物的价值,那么这个宝物就不能拿,那就只能继续保持num和maxValue不变然后继续dfs下去(向下和向右两条路),即上述公式中的第2点。它涵盖的意思是,对于任意不满足map[x][y]>maxValue的点,以它为起点进行dfs都会有两条路

基于这样的分析,我写出了以下代码:

#include<iostream>
using namespace std;

const int MAX=55;
const int MOD=1000000007; 
int n,m,k,ans;
int map[MAX][MAX];

void dfs(int x,int y,int maxValue,int num)
{
	if(x==n && y==m){
		if(num==k || (num==k-1 && maxValue<map[x][y]))
			ans++;
		ans%=MOD;
	}
	if(x+1<=n){	//可以继续向下走 
		if(maxValue<map[x][y]) dfs(x+1,y,map[x][y],num+1);	//当前宝物价值大于前一个则可以拿 
		dfs(x+1,y,maxValue,num);							//当然也可以不拿 
	}
	if(y+1<=m){	//可以继续往右走 
		if(maxValue<map[x][y]) dfs(x,y+1,map[x][y],num+1);
		dfs(x,y+1,maxValue,num);
	} 
}

int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>map[i][j];
	dfs(1,1,-1,0);
	cout<<ans<<endl;
	return 0;
}

这个代码的下场是 42 分(过了 3/7 的数据),而剩下的全部超时。 其实得到这样的结果是必然的,迷宫的最大范围是 50╳50,那么对应在递归树里至少有 50 层,这个深度必 然是会超时的。(我写了一个测试程序,录入一个 13*13 的二维表(cin>>13>>13>>5),然后用上面的 dfs 去遍历,输出最后有多少种取法。最后耗时居然达到了 31.19s!!!这个并不是很大的数在这里跑出来都需要 那么久,然后我又把这个 13 改为 50(极限情况),然后就……反正也不知道它啥时候能算出来)
代码1运行13*13的数据的耗时
总之,这个算法失败了!我们得另辟出路。


——分割线之艰难磨砺——


失败乃成功之母!虽然上面的思路失败了,但是我们在心底是要知道这样的分析是没有问题的,错不在思路,而在于数据范围带来的超时限制。也就是说,我们得优化!
刚才说了一个问题,就是递归深度,这个是给定了的。我们的算法要想优化无非两个方向:
1.剪枝,将某些不必要的dfs略去(但是显然在本题中,不到最后的一(几)个点你是无法确定当前情形下是否能够存在合理的取法,即不能轻易剪枝,或者直接点——不能剪枝)
2.记忆化搜索
这两种方法都能有效降低递归深度,但是在本题就只能采用第2种方案。
细想我们上一个代码,超时的点在于每一次的搜索都会经过一些重复的情况,而这样的情况偏偏又遇到深度较大的情形。这样一来就导致本来dfs的次数和深度就很多,而你又在每一次的dfs中进行过多的重复尝试。比如说对于某个dfs而言,小明所在位置是(x,y),手上共有num件宝贝,且其中价值最大的为maxValue。然后基于这样的一个情况在经过多层次的dfs后求出了在这样的情况下,小明走到终点会有n种取法。(当然,这n种取法指:在各种不同的走法下对应的不同取法的总和)
然后回退,直到比较上层的几个位置。这时你的dfs又以其他形式开始了,然后巧的是,这次你又会经过刚才那个点(x,y),并且此时你手上正好又有num个宝物,以及其中的最大值也刚好又是maxValue。那么你又要傻傻的去继续dfs下去么?
试想,要是这一层dfs还很高,下面还有很多层,那么你又这么dfs下去是不是白白浪费了时间,也浪费了上一次已经算出来的答案?
于是乎,记忆化搜索千呼万唤始出来。

对于前面提到的dfs,其实它正好提出了一个新的表示方式dp[x][y][num][maxValue]
对于这个四维变量,我相信任何人的第一眼看到内心都是极度的不舒服的,因为……是真的太难理解了。
不过不要慌,慢慢来理解。
首先,要知道在这个四维变量引入的时候,其实它就已经枚举了每一种情况(即对每种情况都能用这个四维变量来唯一标识),什么意思?比如令dp[x][y][num][maxValue]=n,那么它的逻辑意义是:在位置(x,y)处已经拿了num个宝物,其中价值最大的是maxValue,而以这样的状态走到终点共有n种取法(这n种取法指:从当前点(x,y)到终点存在某数量的路,对于其中每一条路又有某数量的取法,而n等于这里的所有取法之总和)。前提是,在地宫唯一确定的时候。

前面提到我们最初的那个算法会浪费掉很多的尝试,于是为了让那些已经尝试过的结果能够被保存下来,我们就引入了这个四维数组dp[x][y][num][maxValue]。它存在的意义就是将前面已经dfs过的某个情况给保存下来,以让后来的某个dfs遇到时,能够直接取值而不用再去dfs,从而节约了dfs的时间。“记忆化搜索”也正是这么来的。

根据这样的思路,我将上面的代码进行了相应的改变,同样的测试数据(cin>>13>>13>>5),跑出来的结果却是如此大的差距(0.3547s),如下图:
改进后的算法运行13*13的耗时情况
而直接测试50 50 12(cin>>50>>50>>12)的某组测试数据,居然只多了0.06s的差距,可见算法带来的差距是多么的大啊。
改进后的算法运行50*50的耗时情况


——分割线之炉火纯青——


下面直接给出改进后的算法:

#include<iostream>
#include<cstring>
using namespace std;

const int MAX=55;
const int K=15; 
const int MOD=1000000007; 
int n,m,k;
int map[MAX][MAX];
int dp[MAX][MAX][K][K];	//dp[x][y][num][max]=n表示当在位置x,y时,其手上共有num件宝物
						//其中价值最大的是max,从这个位置到终点共有n种取法(走的路线和拿的策略) 
int dfs(int x,int y,int num,int maxValue)
{
	//记忆化(若在之前遍历过而已经得的了这个情况下的数据时,就不再继续dfs下去了) 
	if(dp[x][y][num][maxValue+1]!=-1) //由于maxValue的初始值必须为-1,在索引里最小为0,因此需要+1 
		return dp[x][y][num][maxValue+1];
	//当到达出口时判断 
	if(x==n && y==m){
		if(num==k || num==k-1&&map[x][y]>maxValue) 
			return 1;
		return 0;
	}
	//两种行走方式 
	long long sum=0;			//用long long(最大为900多亿亿)保险 
	if(x+1<=n){					//如果可以往下走 
		if(map[x][y]>maxValue)	//如果当前手中宝物最大值小于当前正在的这个点的宝物价值 
			sum+=dfs(x+1,y,num+1,map[x][y]);	//则可以拿 
		sum+=dfs(x+1,y,num,maxValue); 			//也可以不拿 
	}
	if(y+1<=m){					//如果可以往右走 
		if(map[x][y]>maxValue)
			sum+=dfs(x,y+1,num+1,map[x][y]);
		sum+=dfs(x,y+1,num,maxValue);
	}
	return dp[x][y][num][maxValue+1]=sum%MOD;
}

int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>map[i][j];
	memset(dp,-1,sizeof(dp));
	dfs(1,1,0,-1);				//特别注意:你的初始价值必须为-1,因为宝物的最小值是包括了0的 
	cout<<dp[1][1][0][0]<<endl;
	return 0;
}
发布了30 篇原创文章 · 获赞 67 · 访问量 3030

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/102971371