[知识点]动态规划基础

ぃ、小莉子 提交于 2020-03-20 16:30:51

一、前言

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

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

 

二、介绍

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

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

 

三、记忆化搜索?动规?

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

不知道动规碰到这道题的唯一方法为暴力搜索——进行DFS,对于每一株有选与不选两个选项,从第 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),太美。

我们来分析下这个搜索的过程:搜索顺序是单调的,从第一株草药开始直到最后一株,也就是说当前如果搜索到第 i 株,那么这个状态与后面的 i + 1, i + 2, ..., m 株都是没有关系的。

我们再注意到,对于每一层dfs,它的三个参数o, ot, ov可以看作这层dfs的一个数据标记,表示着选择完前 o - 1 株草药(并不包括当前的第 o 株,因为还没有选择它是否要采集)时已经耗费的时间ot和已经获得的价值ov。

再来思考一个问题:假设我们在之前的一次选择中,前 i - 1 株在耗费时间为 j 的前提下获得了价值 v1,当前我们dfs又一次在对第 i 株进行选择,同时当前的情况为:耗费了时间 j,且获得了价值 v2,并且v2 < v1,那么是不是意味着不管接下来如果对后面的草药进行选择,最终的结果必然不会超过之前的那次?

答案是显然的呢。

那么,聪明的你一定想到,可以开一个数组f[i][j],用来表示选择第 i 株时,耗费了时间 j,获得的最大价值。对于每次dfs,如果我们之前没碰到过“选择第 i 株耗费时间 j”这个情况,就直接把当前价值记录到f[i][j];而如果已经碰到过了,说明f[i][j]里已经有数据了,则直接将当前价值与f[i][j]进行比较,较大值即为更优解。再来看一下代码:

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

我们没有用ans来存储答案了,因为答案其实已经存在f数组中,根据定义,f[m + 1][1], f[m + 1][2], ..., f[m + 1][T] 中的最大值为最终结果,用个for循环找就是了。

普通的暴力搜索,是没有记忆的;相比之下,我们增加的数组给搜索添加了记忆功能,让他有一点先见之明,而不会明知山有虎偏向虎山行。

称之为——记忆化搜索

说了这么多,这和动态规划有什么关系呢?

 

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

继续研究上面这道例题。首先前面已经说了,我们选择草药是从头到尾依次选择的,是单调的;再来看时间这个维度。

假设当前状态为 f[i][j],即已经对前 i - 1 株进行了选择,同时消耗了时间 j;对于第 i 株,我们有两个选择:采集或者不采集。采集的话,消耗时间 t[i],获得价值 v[i],在时间没超过 T 的前提下,我们递归到下一层的时候,状态必然为 f[i + 1][j + t[i]],且 f[i + 1][j + t[i]] = f[i][j] + v[i]。以此类推,也就是说,我们在递归的时候,时间必然也是一直增加的,即也是单调的。

i 和 j 两个参数都是单调变化的,为何要大费周章地用递归去实现?

我们把前面的思路倒推一下:假设当前状态为 f[i][j],可能是从什么状态递归到这个状态的?两种情况:采集了第 i - 1 株,即从 f[i - 1][j - t[i - 1]] 递归;没采集,即从 f[i - 1][j] 递归。那么,我们完全可以通过递推的方式来实现 f 的求解!看下下面的代码:

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];

为了代码书写更为简洁,我们将 f[i][j] 的定义稍微修改下:f[i][j] 表示前 i 株草药消耗 j 的时间获得的最大价值。同样地,状态转移也就变成:

f[i][j] = max(f[i - 1][j - t[i]] + v[i], f[i - 1][j])

当然,在 j 都不能满足第 i 株有时间被采集的情况下,就默认不采集了,即

f[i][j] = f[i - 1][j]

顺水推舟,可以得到,答案也必然是 f[m][1], f[m][2], ..., f[m][T] 中的最大值。

如果能理解这个递推式了,那么,其实你已经学会最基础的动态规划啦。

 

五、动态规划的核心

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

1、划分状态

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

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

2、状态表示

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

比如例题中的 f[i][j],表示“前 i 株草药消耗 j 的时间获得的最大价值”。

3、状态转移

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

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

4、状态范围

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

 

六、最后

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!