本篇博客要介绍了动态规划的基本思想,以及动态规划中状态及状态转移方程的设计思路,帮助各位初学者对动态规划有一个初步的了解。
本部分的其他页面,将介绍各种类型问题中动态规划模型的建立方法,以及一些动态规划的优化技巧。
数字三角形
给定一个 r 行的数字三角形(r ≤ 1000),需要找到一条从最高点到底部任意处结束的路径,使 路径经过数字的和最大。每一步可以走到当前点左下方的点或右下方的点。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5在上面这个例子中,最优路径是 7 → 3 → 8 → 7 → 5。
最简单粗暴的思路是尝试所有的路径。因为路径条数是 O(2 r) 级别的,这样的做 法无法接受。
注意到这样一个事实,一条最优的路径,它的每一步决策都是最优的。
以例题里提到的最优路径为例,只考虑前四步 7 → 3 → 8 → 7,不存在一条从最 顶端到 4 行第 2 个数的权值更大的路径。
而对于每一个点,它的下一步决策只有两种:往左下角或者往右下角(如果存 在)。因此只需要记录当前点的最大权值,用这个最大权值执行下一步决策,来更新后续点的最大权值。
这样做还有一个好处:我们成功缩小了问题的规模,将一个问题分成了多个规模 更小的问题。要想得到从顶端到第 r 行的最优方案,只需要知道从顶端到第 r − 1 行的最优方案的信息就可以了。
这时候还存在一个问题:子问题间重叠的部分会有很多,同一个子问题可能会被 重复访问多次,效率还是不高。解决这个问题的方法是把每个子问题的解存储下 来,通过记忆化的方式限制访问顺序,确保每个子问题只被访问一次。
上面就是动态规划的一些基本思路。下面将会更系统地介绍动态规划的思想。
能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。
具有最优子结构也可能是适合用贪心的方法求解。
注意要确保我们考察了最优解中用到的所有子问题。
要保持子问题空间尽量简单,只在必要时扩展。
最优子结构的不同体现在两个方面:
子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。
已经求解的子问题,不会再受到后续决策的影响。
如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。
对于一个能用动态规划解决的问题,一般采用如下思路解决:
如果用图论的思想理解,我们建立一个有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题。
最长公共子序列问题
给定一个长度为 n 的序列 A 和一个 长度为 m 的序列 B(n, m ≤ 5000),求出一个最长的序列, 使得该序列既是 A 的子序列,也是 B 的子序列。
子序列的定义可以参考 子序列。一个简要的例子:字符串 abcde 与字符串 acde 的公共子序列有 a、c、d、e、ac、ad、ae、cd、ce、de、ade、ace、cde、acde,最长公共子序列的长度是 4。
设 f(i, j) 表示只考虑 A 的前 i 个元素,B 的前 j 个元素时的最长公共子序列的长 度,求这时的最长公共子序列的长度就是 子问题。f(i, j) 就是我们所说的 状态, 则 f(n, m) 是最终要达到的状态,即为所求结果。
对于每个 f(i, j),存在三种决策:如果 Ai = Bj,则可以将它接到公共子序列的末 尾;另外两种决策分别是跳过 Ai 或者 Bj。状态转移方程如下:

该做法的时间复杂度为 O(nm)。
另外,本题存在 O ( nm / w ) 的算法 。有兴趣的同学可以自行探索。
- int a[MAXN], b[MAXM], f[MAXN][MAXM];
- int dp() {
- for (int i = 1; i <= n; i++)
- for (int j = 1; j <= m; j++)
- if (a[i] == b[j])
- f[i][j] = f[i - 1][j - 1] + 1;
- else
- f[i][j] = std::max(f[i - 1][j], f[i][j - 1]);
- return f[n][m];
- }
先说这么多,还有一道例题《最长不下降子序列》下一篇再讲,请多多指教。