[算法练习]蓝桥杯历届试题 地宫取宝 超级详细的解题过程 C++

历届试题 地宫取宝

问题描述

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

解题思路

这个题还是很不错的,整整用了我两个晚上的时间,前面做的有一题用到了动态规划,导致我现在遇到什么题都想动态规划,刚拿到这道题,第一反应是想动态规划,随后不到十分钟,这个念头被我打消了,我们来看看题目的要求,入口左上角,出口右上角,只能向下向右走,每个单元格上有一个宝物,若想要拿起宝物,拿起的宝物必须比手上所拥有的任一一个宝物价值都大,每个单元格都可以选择拿或者不拿,问最后到终点能拿到K个宝物的行动方案,条件有点丰富,比起平时简单的迷宫题地图题,这道题似乎多了许多独特的味道,于是开始分析,什么是行动方案,就是一个单元格,如果选择了拿起和不拿起,这都是两个不同的方案了,那么问题的意思我们理清楚了,那么怎么下手呢,由于本人还处于一个极其菜鸡的阶段,虽然DFS、BFS什么的在算法与数据结构课程中已经学习过并且明确其定义,但是说实话,并不知道怎么在这种针对问题的编程中使用起来,后面会专门找相关的题目练习,而关于本题,经过一顿搜索网上给出的思路是DFS+DP+记忆化搜索,真好,除了记忆化搜索都不知道怎么用,怎么能用网上别人的思路呢,那是自己的东西吗,于是我开始了细想,我是不是可以定义一个二维数组,来表示整个地图,针对每一个地图点,我设置一个可变数组Vector来存储这个地图点可能的所有信息,然后每一个Vector元素存储一个二元组数据,分别是当前拥有的宝物个数以及当前拥有的最大的宝物个数,然后不断遍历每个地图点,到终点的时候,将所有当前拥有宝物个数为所求K的元素计数,就得到了所有可能的行动方案,很明显这种思路是不行的,但是倔强的我觉得,蓝桥杯比较水,我全部情况枚举出来应该没事吧,于是编写代码如下:

#include<iostream>//这是错误代码
using namespace std;
#include<vector>
#include<string>
class situ{
	private:
		int num;//当前到此格的已有宝物数量 
		int max;//当前所拥有的最大值 
	public:
		situ(int a,int b){
			num=a;max=b;
		}
		int get_num(){
			return num;
		}
		int get_value(){
			return max;
		}
};
vector<situ> dp[50][50];
int value[50][50];
int main(){
	int n,m,k;cin>>n>>m>>k;
	for(int i=0;i<n;i++){//录入数据 
		for(int j=0;j<m;j++){
			cin>>value[i][j];
		}
	}
	//初始值
	dp[0][0].push_back(situ(0,0)); 
	dp[0][0].push_back(situ(1,value[0][0]));
	//循环遍历
	for(int i=0;i<n;i++){
		for(int j=0;j<m;j++){
			if(i!=n-1){
				for(int a=0;a<dp[i][j].size();a++){
					if(dp[i][j][a].get_value()<value[i+1][j]){
						dp[i+1][j].push_back(situ(dp[i][j][a].get_num()+1,value[i+1][j]));//捡起宝物 
						dp[i+1][j].push_back(situ(dp[i][j][a].get_num(),dp[i][j][a].get_value()));//没捡宝物 
					}
					else{
						dp[i+1][j].push_back(situ(dp[i][j][a].get_num(),dp[i][j][a].get_value()));//没捡宝物
					}
				}
			}
			if(j!=m-1){
				for(int b=0;b<dp[i][j].size();b++){
					if(dp[i][j][b].get_value()<value[i][j+1]){
						dp[i][j+1].push_back(situ(dp[i][j][b].get_num()+1,value[i][j+1]));//捡起宝物 
						dp[i][j+1].push_back(situ(dp[i][j][b].get_num(),dp[i][j][b].get_value()));//没捡宝物 
					}
					else{
						dp[i][j+1].push_back(situ(dp[i][j][b].get_num(),dp[i][j][b].get_value()));//没捡宝物
					}
				}
			}
		}
	}
	int times=0;
	for(int i=0;i<dp[n-1][m-1].size();i++){
		if(dp[n-1][m-1][i].get_num()==k){
			times++;times=times%1000000007;
		}
	}
	cout<<times<<endl;
	return 0;
} 

果不其然,第一个晚上按照自己夸张的思路,编写出来的代码失败了,我相信大部分初学者,应该都会有这样的思路,题目需要什么,我就编写什么,所有情况爷都给你整出来再计数,最终结果必然如下:
第一次尝试
于是本人痛定思痛,结合前面刚刚学会还热乎的动态规划的思想,动态规划就是把冗余数据都去掉,尽量只计算与我们问题答案有关的数据,于是我发现,你无论K(拥有的宝物数量)是多少,无论拥有的宝物的最大价值多大,最终组成的结果的永远只有最多只有K*12个,因为题目给的数值范围就这么大,前面的方法为什么会运行错误,因为情况太多了,内存溢出了,那么既然有海量的重复数据,我们给它们各自编号计数,岂不是美滋滋,于是在上一种思路的优化上,我们针对每一个地图点,都给一个二维数组,于是这个四维数组就出来了,那么每一个地图点的二维数组代表什么呢,假设这个二维数组的元素为arr[i][j],那么它的数值就代表在走到这个地图点时,拥有i个宝物,最大宝物价值为j的可能方案有arr[i][j]种,那么有了这个想法,我们针对以上代码进行修改(实际上是重新写了份),最终得到了令人满意的答案
第二次尝试
这熟悉的正确率,仿佛我辛辛苦苦边看爱情公寓边聊天边吃辣条敲出来的代码就这么没了,爷不服输,放下辣条,关掉爱情公寓,把所有二维数组都给输出出来看看,硬生生看了两个多小时,这思路没错啊,很棒啊,你看都不超时了,全是错误了,最后回来又仔细看了遍题目 ,宝物价值>=0,似乎这点被我忽视了,于是修改了代码,再次提交,爷真棒啊!
第三次尝试
总得来说,这道题的确有点艰辛,菜鸡本鸡,连DFS和BFS都不能用起来,等于白学,后面得找题练练,不然白学多垃圾,这次题目的解题也不知道是啥方法,反正挺快乐,虽然有四个循环,但其实循环的值很小到最后也不会超时,并且这道题数据量也不能再大,因为现在题目限制的数据量已经十分巨大了,所以优化的必要是没有的,当然肯定是有优化的可能的,菜鸡我当然是不多想啦

答案

#include<iostream>
using namespace std;
#include<vector>
#include<string>
#include<iomanip>
long long dp[51][51][51][51];
int value[51][51];
int main(){
	int n,m,k;cin>>n>>m>>k;
	for(int a=0;a<51;a++){//设置dp数组初值 
		for(int b=0;b<51;b++){
			for(int c=0;c<51;c++){
				for(int d=0;d<51;d++){
					dp[a][b][c][d]=0;
				}
			}
		}
	}
	for(int a=0;a<51;a++){
		for(int b=0;b<51;b++){
			value[a][b]=0;
		}
	}
	for(int i=0;i<n;i++){//录入数据 
		for(int j=0;j<m;j++){
			cin>>value[i][j];value[i][j]++;
		}
	}
	//初始值
	dp[0][0][0][0]=1; 
	dp[0][0][1][value[0][0]]=1;
	//循环遍历
	for(int a=0;a<=n;a++){
		for(int b=0;b<=m;b++){
			for(int c=0;c<=k;c++){//最多只能拥有k件宝物 否则丢弃数据 
				for(int d=0;d<=13;d++){//最多只能有12的最大价值
					if(dp[a][b][c][d]>0){//代表有这个数据存在
									if(d<value[a][b+1]){
										dp[a][b+1][c][d]+=dp[a][b][c][d]%1000000007;
										dp[a][b+1][c+1][value[a][b+1]]+=dp[a][b][c][d]%1000000007;
									}
									else{
										dp[a][b+1][c][d]+=dp[a][b][c][d]%1000000007;
									}
									if(d<value[a+1][b]){//小于表示可以捡也可以不捡
										dp[a+1][b][c][d]+=dp[a][b][c][d]%1000000007;
										dp[a+1][b][c+1][value[a+1][b]]+=dp[a][b][c][d]%1000000007;
									}
									else{
										dp[a+1][b][c][d]+=dp[a][b][c][d]%1000000007;
									}
					}
				}
			}
		}
	}
	int times=0;
	for(int i=0;i<50;i++){
		times=(times+dp[n-1][m-1][k][i])%1000000007;
	}
	cout<<times<<endl;
	return 0;
} 

评测记录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值