引言

这是一个悬壶济世的故事。据说神农尝百草后,将自己的收获撰写成一本书,分发给各个部落的长老。有一天,一个部落里面忽然爆发了各种疾病,为了遏制疾病,部落长老出去寻找能治病的草药。历经千辛万苦,他终于找到了一片福地,这里面的草药应有尽有,然而,有些药近在眼前,能很容易地找到;有些药则远在天边,需要花费很多天才能找到。假设长老知道所有草药的位置,并且能确定采到药的时间,每种药只能救活特定人数的人,且他只有有限的时间用来寻找草药,那么,他该如何采药,才能救活更多的人呢?

正文

首先,我们可以确定的是,爆搜肯定能帮助长老找到最优解,但长老很赶时间,等到爆搜完,村里的人可能都死光光了~所以要找到时间更短的方法。我们思考这样一个问题:总共只有这么几种药,几块地方,爆搜搜来搜去肯定会有一模一样的情况出现。如果我们记住第一次搜索这种情况的答案,等到第二次搜索的时候直接用,是不是就能有效减少搜索的时间了呢?我们按照这个思路写一下:

爆搜思路

如果爆搜的话,我们就直接考虑这个题涉及到多少变量就可以了。显然,题目中包含三种变量:

  1. 搜索每种草药的时间
  2. 每种草药能救活多少人
  3. 长老采药的时间限制
    我们直接爆搜这三种变量就可以:

代码

#include<algorithm>
#include<iostream>
using namespace std;
int* time1;//采每种药花费的时间
int* price;//每种药的价值
int sum_time=100;//长老剩余的采药时间
int now_val=0;//现有草药的价值
int n;//草药种数
int ans;//现在的解
int dfs(int i,int sum_time,int now_val)
{
	if (sum_time < 0)return;
	if (i == n + 1) {//每次搜索完如果符合条件则更新解
		ans = max(ans, now_val);
		return;
	}
	//依次枚举采该种药和不采该种药的情况
	dfs(i + 1, sum_time, now_val);
	dfs(i + 1, sum_time - time1[i], now_val + price[i]);
}

记忆化搜索思路

爆搜算法的时间复杂度是指数级的,肯定行不通。于是我们按照上文思路,记住上一次搜索的答案,在下一次直接用就可以。另外当前采药的价值是由前两种变量决定的,因此可以只搜索两个变量,每次将第三个变量的答案通过数组存起来就可以了。
这种减少维度的思想在合并石头的最低成本中也用到过。

代码

#include<algorithm>
#include<iostream>
using namespace std;
int* time1;//采每种药花费的时间
int* price;//每种药的价值
int sum_time=100;//长老剩余的采药时间
int** now_val;//用二维数组来存储第i种草药采或不采的价值
int n;//草药种数
int ans;//现在的解
int dfs(int i,int sum_time)//搜索第i种草药,省去了第三个变量,改用数组存储。
{
	if (sum_time < 0)return;
	if (now_val[i][sum_time] != -1) {
		return now_val[i][sum_time];
	}
	if (i == n + 1)now_val[i][sum_time] = 0;
	int dfs1, dfs2;
	dfs1 = dfs(i + 1, sum_time);
	dfs2 = dfs(i + 1, sum_time - time1[i])+price[i];
	return now_val[i][sum_time] = max(dfs1, dfs2);
}

递推思路

分析一下,这道题满足动态规划的三个特征:

  1. 最优子结构:大问题的最优解是所有小问题最优解的最大值
  2. 无后效性:不能回头去采已经采过的药
  3. 重叠子问题:我们分析记忆化搜索的时候就知道肯定会有完全一样的子问题。
    因此可以用动态规划的方法解决。

代码

#include<algorithm>
#include<iostream>
using namespace std;
int* time1;//采每种药花费的时间
int* price;//每种药的价值
int sum_time=100;//长老剩余的采药时间
int** now_val;//用二维数组来存储第i种草药采或不采的价值
int n;//草药种数
int ans;//现在的解
int dp()
{
	for (int i = 0; i < n; i++) {
		for (int j = sum_time; j >=0; j++) {//起始状态是从第0种草药开始选择,剩余时间为初始的sum_time
			if (j >= time1[i]) {
				now_val[i][j] = max(now_val[i - 1][j], now_val[i - 1][j - time1[i]] + price[i]);//不采或采
			}
			else {
				now_val[i][j] = now_val[i - 1][j];//只能选择不采
			}
		}
	}
}

总结

简要比较一下记忆化搜索和递推代码,你会发现他们的答案都是通过比较两个东西的最大值来得出的,而且这两个东西都是之前求好的,所以他们的思维方式及其相似,都使用了空间换时间的思想,时间复杂度也是差不多的。
与动态规划相比,记忆化搜索比较好写,无需考虑状态转移方程,而且处理边界比较方便(上文的记忆化只用了一个if条件来处理)。但是由于存在递归,因此效率会低于动态规划,而且难以优化,需要取舍。

如何写出记忆化搜索

上文是从爆搜代码优化到记忆化搜索的,我们可以总结为:

  1. 写出爆搜代码
  2. 减小维度,去除不必要变量
  3. 写出存储数组

当然也可以从动态规划到记忆化搜索:

  1. 写出dp代码
  2. 用状态转移方程写出对应的dfs函数
  3. 写出存储数组

————————分割线——————————

我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!码文不易,如果觉得好的话,可以关注一下,我会在将来带来更多更全面的算法讲解!

04-18 00:38