[知识点] 4.1 记忆化搜索与动态规划

总目录 > 4 动态规划 > 4.1 记忆化搜索与动态规划

前言

最近又做了一些比较基础的 DP,感觉自己无敌了,应该有资格写篇文章来介绍了!

本文主要介绍动态规划的概念,记忆化搜索以及动态规划的核心。

更新日志

Update - 20200616

写完搜索部分后再回头看了下这篇起到衔接作用的文章,发现对记忆化搜索的概念还是有点偏差,加上之前在记忆化搜索和动规的过渡本身就有点牵强,于是进行了大幅修改。

同时划上了许多重点。

子目录列表

1、介绍

2、记忆化搜索?动规?

3、不要递归的记忆化搜索

4、动态规划的核心

5、总结

 

4.1 记忆化搜索与动态规划

1、介绍

动态规划 (dynamic programming) 是运筹学的一个分支,是求解决策过程 (decision process) 最优化的数学方法。其本质是将一个复杂的问题拆分成若干个相对简单的子问题,所以常适用于有重叠子问题最优子结构问题

这样介绍动态规划是很空洞而抽象的,我们从更简单的方式切入。

 

2、记忆化搜索?动规?

Luogu P1048】【NOIP2005】【采药】山洞里有 m 株不同的草药,第 i 株草药的价值为 v[i],采第 i 株草药需要时间 t[i]。在时间 T 内,要求采集一些草药,使总价值最高。

不知道动规碰到这道题的唯一方法为暴力搜索 —— 进行 DFS(请参见:3.1 DFS / BFS 搜索),对于每一株有选与不选两个选项,从第 1 株开始逐一进行二选一,如果出现时间超过限定则回溯;直到对 m 株都进行了选择,记录当前的价值并和最大价值比较,选择较大值,以此反复,可得最优解。核心代码如下:

1 void dfs(int o, int ot, int ov) {
2     if (ot > T) return;
3     if (o == m + 1) {
4         ans = max(ans, ov);
5         return;
6     }
7     dfs(o + 1, ot, ov);
8     dfs(o + 1, ot + t[o], ov + v[o]);
9 } 

时间复杂度为 O(2 ^ n),太美。

它的三个参数 o, ot, ov 可以看作这层 DFS 的状态,表示着选择完前 o - 1 株草药(并不包括当前的第 o 株,因为还没有选择它是否要采集)时已经耗费的时间 ot已经获得的价值 ov

先不谈如何优化,来讲讲一个概念 —— 外部变量,进行铺垫。外部变量,即在 DFS 函数外定义且随 DFS 过程发生变化的变量。如上代码,ans 即为一个外部变量。ans 是用来记录最终答案的,我们尝试着能不能以另一种形式记录答案 —— 返回值。将 DFS 中的第三个参数 ov 移除,并将函数更改为 int 类型,则 dfs(o, ot) 表示在时间 ot 内采集后 i 个草药获得的最大收益,再来看代码:

1 int dfs(int o, int ot) {
2   if (o == m + 1) return f[o][ot] = 0;
3   int res1, res2 = -INF;
4   res1 = dfs(o + 1, ot);
5   if (ot >= t[o]) res2 = dfs(o + 1, ot - t[o]) + v[o];
6   return f[o][ot] = max(res1, res2);
7 }

这时,res1 保存的是不采集第 o 株的返回值,res2 保存的是采集第 o 株的。而最终的答案,就是 dfs(m, T) 的返回值,通过回溯,最后返回给主函数。

上述两份代码是等价的,那为什么要提后面这种呢?我们尝试着去记录每一层 DFS 的返回值,震惊地发现,对于相同的 o, ot,其返回值也是相同的!其实也不难理解,因为本身不再借助外部变量,所以 DFS 函数是完全独立的运行,不受外界影响,对于相同的参数,其返回值相同也是必然的。

那么,利用这一点,我们就可以对搜索进行大幅度的优化了!对于 dfs(o, ot),如果之前已经出现过 (o, ot) 这个状态了,那么大可不必再次进行后续的计算,直接将之前得到的返回值拿来就行,所以我们可以定义一个数组 f[i][j] 用以记录 dfs(i, j) 的返回值,每次递归时先判断 f[i][j] 是否已经有返回值,如果没有则继续执行,如果有则直接跳过。

所以,在上述代码的基础上增加一行即可:

1 int dfs(int o, int ot) {
2   if (f[o][ot] != -1) return f[o][ot];
3   if (o == m + 1) return f[o][ot] = 0;
4   int res1, res2 = -INF;
5   res1 = dfs(o + 1, ot);
6   if (ot >= t[o]) res2 = dfs(o + 1, ot - t[o]) + v[o];
7   return f[o][ot] = max(res1, res2);
8 }

普通的暴力搜索,是没有记忆的;相比之下,我们增加的数组给搜索添加了记忆功能,使其不会走弯路,吃一堑长一智,这样的搜索,我们称之为 —— 记忆化搜索

它本身属于 3.3 搜索优化 中的一种,同时又和动态规划有着极其紧密的联系,用于搜索和动规之间的衔接最合适不过了。那么,它和动态规划是什么关系呢?

 

3、不要递归的记忆化搜索

还是上面这道例题。首先前面给出了一个定义 —— dfs(o, ot) 表示在时间 ot 内采集后 o 个草药获得的最大收益,同时 f[i][j] 记录的则是 dfs(i, j) 的返回值。

通过递归过程的分析不难发现,对于参数 o,其递归顺序是单调的,从最后一株草药一直到第一株,也就是说在对第 i 个草药进行采集还是不采集的选择时,只和第 i - 1 个(从后 i 个递归到后 i + 1 个,即相当于从第 i 个草药递归到第 i - 1 个草药)草药存在直接的转移关系(也正是因为这个特性,这道题的动规方程可以只使用一维数组,即滚动数组,但这里不提,在 4.2 背包 DP 会提及)。

而对于参数 ot,随着草药采集的增多,ot 必然是变小的。对于第 i 个草药,如果选择采集,那么返回值就存在 f[i][j] = f[i - 1][j - t[i]] + v[i] 的关系;如果不采集,f[i][j] = f[i - 1][j]。也就是说,o 和 ot 两个参数在递归的过程中都是单调变化的,为何要大费周章地用递归去实现?

看下这段代码。

1 for (int i = 1; i <= m; i++)
2     for (int j = 1; j <= T; j++)
3         if (j >= t[i]) 
4             f[i][j] = max(f[i - 1][j - t[i]] + v[i], f[i - 1][j]);
5         else 
6             f[i][j] = f[i - 1][j];

通过简单的二重循环,圆满完成了递归完成的任务 —— 两个参数都是单调的,for 循环完全可以实现。

而使用的这个 f 数组,恰好就是递归过程中使用的记录数组,即 f[i][j] 表示对于后 i 个草药在时间 j 内获得的最大价值,最终的结果为 f[n][1..T] 中的最大值,因为并未限定时间必须为多少。那么这样一个数组,在动态规划中,我们称之为状态数组,即用来表示由若干个参数组成的状态值;而诸如 f[i][j] = max(f[i - 1][j - t[i]] + v[i], f[i - 1][j]) 这样的式子,我们称之为状态转移方程,用于建立起相关状态间的联系。

你可以说,记忆化搜索约等于动态规划,也可以说,记忆化搜索是一种以递归 / 搜索形式实现的动态规划,总而言之,两者是密不可分的。但同时也并非完全等同。

相比之下,记忆化搜索可以避免搜索到一些无用的状态,所以它也是被视作搜索优化的一种;更好理解,没有动规那么抽象。缺点的话,首先是递归形式,效率相比 for 循环肯定是低的;不能使用滚动数组来降低维度,空间复杂度难以优化;代码量较大。

 

4、动态规划的核心

从上面的例题中,我们提炼出动态规划的几个核心:

① 划分状态

一项任务能不能用动态规划的思想来完成,首先判断能否或者是否比较轻松的划分出子问题,以定义每个子问题的状态。

比如例题中草药,时间,价值,以及之间的关系。

② 状态表示

几乎所有动态规划离不开一个 f 数组。f 数组是一个抽象数组,它并没有例题中“v[i] 表示第 i 株草药的价值”这么直白的定义,而是以其若干个维度表示的参数所组成的一个状态。

比如例题中的 f[i][j]。

③ 状态转移

如何从一个状态转移到另一个状态是动态规划的关键,明确了状态转移方程,才能顺水推舟地一路递推下去,直到获得结果。

例题中的状态转移方程见上。

④ 状态范围

初始状态是什么?每一个参数的范围在哪里?最终状态又是什么?正如你已经知道了前行的路径,还需要定义起点和终点才能出发。

比如例题中,我们从最后一株草药开始选择是否采集,时间则是从 0 开始。

 

5、最后

动态规划最基础的概念就是这些,而这仅仅只是一些皮毛,接下来还有动态规划的各种基本模型,以及进一步拓展的各种高级动态规划,以及动态规划的优化方法。

posted @ 2020-03-20 15:14  jinkun113  阅读(1879)  评论(0编辑  收藏  举报