问题:1436 地宫取宝——记忆搜索/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 3 2
1 2 3
2 1 5

样例输出:
14
文章底部附加两组正确数据用以测试。

解题过程:
刚开始看到这道题的时候我自己想的是dfs(其实也有用bfs实现过),但是实现之后放到蓝桥杯上测试都超时了,自己拿了另外一组数据去测试也确实是要用很长的时间,因为那个时候刚学搜索算法,所以就一直在找超时的原因,当时也不知道是哪根经抽了,一直认为不是重复计算的原因,然后就一直在做影响不大的修改,直到后来实在不行去查了别人的解法,发现有一种叫做记忆搜索的算法,当时还不知道“这个名字”的这种算法,然后还没看别人的代码就先去查了一下这种算法,看了之后才知道原来就是加个备忘录而已,我寻思着这样能解那不就是代表着有重复计算问题了吗?
一开始一直认为没有重复计算的问题就是因为我想的是从原点到终点的每条路径肯定都是不一样的,这样怎么可能会重复计算呢?即使我到达了之前有到达过的某个点,并且我身上的宝物件数和宝物价值的最大值都跟之前的一模一样,但是前面的路径肯定是不同的,这样又怎么会重复计算呢?——我都不明白我为什么会有这么蠢的问题(后来知道真相的我眼泪掉下来,真的蠢到家了T﹏T)。
在查别人解法的时候还发现了用dp的解法。后面会说这种解法。

解题思路:
dfs就不用多讲了。主要要解决的就是其中重复计算的问题,当我们第二次到达某个点时,如果当前到达的状态(所带宝物件数和宝物价值的最大值)与之前到达该点时的状态是相同的时候,那么接下来的计算就都是跟之前那次相同状态的计算一模一样了。
举个栗子:小明想要知道从自个家里去学校的路径条数,小红知道从自己家去到学校的路径条数,并且小明去学校的路上必定要经过小红家,那么小明只需要计算从自己家去到小红家的路径条数,因为当他以某一条路径到达小红家时,就已经可以得出这条路径到学校的路径条数了(当然前提是得小红愿意告诉他,哈哈哈)。不知道这样的解释你能不能理解,不过我是理解了。
这里面有个地方也需要非常注意,就是要把所有宝物的价值都+1,代码中会有解释。我因为没有+1,在测试的时候一直出错,而且找原因找了特别久。

上代码:

#include<iostream>
#include<vector>
#include<cstring>

class State
{
public:
	State():x(-1),y(-1),amounts(0),maxValue(0){ }
	State(int r, int c, int a, int v):x(r),y(c),amounts(a),maxValue(v){ }
public:
	int x, y;
	int amounts;   //珠宝的数量
	int maxValue;  //最大价值的珠宝的价值
};

std::vector<std::vector<int>> graph;
int gr = 0, gc = 0; //图的行列
int target = 0;     //目标件数
long long solutions = 0; //解决方案的种数
//备忘录:mem[x][y][a][v] 
//代表从(x,y)且在该点所带宝物件数为a,宝物中价值的最大值为v,到终点符合条件的路径条数
long long mem[51][51][14][14]; 

void dfs(const State &s) {
	if (mem[s.x][s.y][s.amounts][s.maxValue] != -1) {
		solutions += mem[s.x][s.y][s.amounts][s.maxValue];
		return;
	}
	if (s.x == gr - 1 && s.y == gc - 1) {
		if (s.amounts == target) ++solutions;
		return;
	}
	long long pre_rise = solutions;
	//需要的件数 - (目前这个位置到最终位置所能拿到的最大件数 + 目前拥有的件数) > 0  符合这个情况就不用再继续向下查找
	if (target - (s.amounts + (gr - 1 - s.x) + (gc - 1 - s.y)) > 0) return;
	//如果宝物价值没有+1,在遇到价值为0宝物,并且自身没有携带宝物时,是没有办法拿到这个宝物的。
	if (s.x + 1 < gr) {
		if (s.amounts + 1 <= target && graph[s.x + 1][s.y] > s.maxValue)//拿
			dfs(State(s.x + 1, s.y, s.amounts + 1, graph[s.x + 1][s.y]));
		dfs(State(s.x + 1, s.y, s.amounts, s.maxValue)); //不拿
	}
	if (s.y + 1 < gc) {
		if (s.amounts + 1 <= target && graph[s.x][s.y + 1] > s.maxValue)
			dfs(State(s.x, s.y + 1, s.amounts + 1, graph[s.x][s.y + 1]));
		dfs(State(s.x, s.y + 1, s.amounts, s.maxValue));
	}
	mem[s.x][s.y][s.amounts][s.maxValue] = solutions - pre_rise;  //记录
}

void contact() {
	std::cin >> gr >> gc >> target;
	graph.resize(gr);
	for (int i = 0; i < gr; ++i) {
		graph[i].resize(gc);
		for (int j = 0; j < gc; ++j) {
			std::cin >> graph[i][j];
			++graph[i][j];  //宝物价值+1
		}
	}
	memset(mem,-1,sizeof(mem));
	dfs(State(0, 0, 0, 0));
	dfs(State(0, 0, 1, graph[0][0]));
	std::cout << solutions % 1000000007 << std::endl;
}

int main() {
	contact();

	return 0;
}

接下来说一说dp解法的过程
说实话在看到别人这个解法的时候我看到那个状态数组是四维的我就不想看下去了,因为我才刚学完dp不久,做过dp的题又都是顶多就三维的,还有就是看到的时候我dfs解法还没搞定,所以也就不是很想看。后来解决dfs解法之后再去看,发现其实也不难,最后用dp实现了之后,也对dp有了更加深刻的认识,也不会觉得畏惧高维的了,认清了其实也就是对应着状态而已,并没有想象中的那么难理解。当然如果你没有了解过dp算法的话,我不建议你用这道题来当入门。

这道题为什么可以用dp来解:
①当前状态的最大值可以由之前的状态的最大值来推导得出;
②当前状态的最大值的推导不会影响到之前的状态;
③有重复子问题。

状态的定义:跟上一道题的men数组差不多。
ways[x][y][a][v] : 代表从原点到(x,y)后所带宝物件数为a,最大价值为v的路径条数。这是从原点到(x,y)的路径条数。
状态方程 :ways[x][y][a][v] = ways[x - 1][y][a][v](从上边继承) + ways[x][y-1][a][v](从左边继承)——这计算的是不拿当前位置宝物的
在计算ways[x][y][a][v]后还需要判断此时可不可以拿当前位置的宝物,以此来计算ways[x][y][a + 1][nv]

#include<iostream>
#include<vector>

std::vector<std::vector<int>> graph;
int gr = 0, gc = 0; //图的行列
int target = 0;
long long solutions = 0; //解决方案的种数
int maxV = 0; //在输入数据的时候记录珠宝中的最大价值

//原点到位置(x,y)拥有amount件珠宝且珠宝中价值最大为maxValue的路径条数
void dp_solve() {
	std::vector<std::vector<std::vector<std::vector<long long>>>> ways;
	ways.resize(50);
	for (int i = 0; i < 50; ++i) {
		ways[i].resize(50);
		for (int j = 0; j < 50; ++j) {
			ways[i][j].resize(13);
			for (int k = 0; k <= 12; ++k)
				ways[i][j][k].resize(14,0);
		}
	}
	ways[0][0][0][0] = 1; ways[0][0][1][graph[0][0]] = 1;
	//初始化第一行
	for (int i = 1; i < gc; ++i) {
		for (int j = 0; j <= i + 1 && j <= target; ++j) {
			for (int v = 0; v <= maxV; ++v) {
				long long count = ways[0][i - 1][j][v];
				if (count != 0) {
					ways[0][i][j][v] += count; //不拿当前位置的珠宝
					if (graph[0][i] > v && j + 1 <= target) ways[0][i][j + 1][graph[0][i]] += count;  //拿当前位置的珠宝
				}
			}
		}
	}
	//初始化第一列
	for (int i = 1; i < gr; ++i) {
		for (int j = 0; j <= i + 1 && j <= target; ++j) {
			for (int v = 0; v <= maxV; ++v) {
				long long count = ways[i - 1][0][j][v];
				if (count != 0) {
					ways[i][0][j][v] += count;  //不拿
					if (graph[i][0] > v && j + 1 <= target) ways[i][0][j + 1][graph[i][0]] += count;  //拿
				}
			}
		}
	}
	for (int r = 1; r < gr; ++r) {
		for (int c = 1; c < gc; ++c) {
			for (int j = 0; j <= r + c + 1 && j <= target; ++j) {
				for (int v = 0; v <= maxV; ++v) {
					long long ucount = ways[r - 1][c][j][v], lcount = ways[r][c - 1][j][v];
					if (ucount != 0) { //上边来的路线条数
						ways[r][c][j][v] += ucount; //不拿当前位置的珠宝
						if (graph[r][c] > v && j + 1 <= target) ways[r][c][j + 1][graph[r][c]] += ucount; //拿当前位置的珠宝
					}
					if (lcount != 0) { //左边来的路线条数
						ways[r][c][j][v] += lcount; //不拿
						if (graph[r][c] > v && j + 1 <= target) ways[r][c][j + 1][graph[r][c]] += lcount; //拿
					}
				}
			}
		}
	}
	//计算符合条件的路径总条数
	for (int val = 0; val <= maxV; ++val)
		solutions += ways[gr - 1][gc - 1][target][val];
}

void contact() {
	std::cin >> gr >> gc >> target;
	graph.resize(gr);
	for (int i = 0; i < gr; ++i) {
		graph[i].resize(gc);
		for (int j = 0; j < gc; ++j) {
			std::cin >> graph[i][j];
			++graph[i][j];
			if (graph[i][j] > maxV) maxV = graph[i][j];
		}
	}
	dp_solve();
	std::cout << solutions % 1000000007 << std::endl;
}

int main() {
	contact();

	return 0;
}

这道题给我的收获还是很多的,对我来说是一道很不错的题目,希望我的博客能给有需要的人带来指点迷津的作用,当然我这代码肯定不是最好的解决过程,肯定是有挺多地方可以再好好优化的,毕竟我看别人说他用dp解决的过程只用了6ms,而我这个dp的解决时间跟6ms比差了非常多,后面有时间我会找找原因,如果有大神可以指点一下当然是最好的啦。
继续加油!!!💪

8 7 8
8 6 3 9 4 8 6
1 7 6 9 0 1 3
4 3 9 4 8 7 5
3 1 5 6 2 2 0
3 4 2 9 2 3 4
9 4 3 1 9 4 8
5 3 9 7 9 6 6
2 7 8 8 1 7 9 输出:5

20 18 6
0 7 2 3 9 1 11 9 7 11 6 9 11 6 10 8 11 6
0 5 4 9 6 9 10 10 1 0 7 1 10 8 0 9 6 5
4 10 10 9 1 9 6 2 4 1 0 6 4 7 3 7 10 7
0 3 11 4 5 9 3 10 9 4 1 7 7 8 10 9 2 11
10 1 4 5 5 10 1 7 6 0 11 5 10 3 3 6 3 5
3 4 3 2 6 0 11 5 3 3 3 4 2 10 3 0 11 11
6 3 5 8 10 9 6 3 5 0 3 7 7 11 6 6 3 6
6 4 2 11 4 7 9 8 7 7 7 0 9 11 0 7 0 9
7 2 9 11 2 6 0 10 5 11 4 7 8 0 6 3 3 10
8 3 6 10 6 9 6 10 8 6 9 10 1 5 6 3 4 2
1 5 9 2 3 6 11 10 5 2 7 10 1 8 11 8 6 0
0 5 1 7 1 8 8 8 9 0 2 0 11 3 10 9 8 0
9 10 0 8 6 2 8 8 4 6 7 2 11 0 4 3 7 9
0 8 2 5 9 3 11 2 9 0 6 6 11 11 1 1 10 8
4 6 0 2 1 8 6 4 7 5 5 2 0 2 5 10 1 6
6 2 1 11 4 9 0 8 7 4 5 10 11 11 6 1 6 4
7 3 10 3 10 10 10 6 9 9 11 7 0 9 11 0 1 5
6 11 9 4 3 6 4 0 9 0 4 11 7 1 2 1 1 10
6 1 4 5 9 9 7 3 3 7 11 2 10 6 8 6 3 4
9 10 3 9 6 4 7 11 3 9 7 4 8 10 1 5 7 0 输出:761196661

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
鉴于提供的内容是一段包含三个不同的Java程序的代码,我无法提供一个完整的答案来回答你的问题。但是,我可以为你提供一些信息。 根据引用和引用的内容,这两个程序都涉及到动态规划的算法。引用中的程序解决了一个关于序列的最长递增子序列的问题,而引用中的程序解决了一个关于背包问题的二维动态规划的问题。 对于引用中的内容,它是一个程序解决的是一个最长上升子序列的问题。 根据提供的代码,这些程序可能是用来解决一些蓝桥杯竞赛的题目。然而,我无法提供更多关于具体问题的信息。 如果你需要帮助解决特定的问题,我建议你仔细阅读每个程序的代码并理解它们的功能,然后根据需要进行相应的修改。如果你有关于动态规划或蓝桥杯竞赛的更具体的问题,我会很愿意帮助你。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [蓝桥杯AcWing学习笔记 3-2简单DP的学习(附相关蓝桥真题:地宫取宝、波动数列)(Java)](https://blog.csdn.net/weixin_53407527/article/details/123122245)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值