算法导论(第15章 动态规划)*
第15章 动态规划
动态规划(dynamic programming)与分治方法相似,都是通过组合子问题的解来求解原问题(在这里,“programming”指的是一种表格法,并非编写计算机程序)。
分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。
动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治算法会做许多不必要的工作,它会反复求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而避免每次求解一个子子问题时都重新计算。
动态规划方法通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。
通常按如下四个步骤来设计一个动态规划算法:
- 刻画一个最优解的结构特征。
- 递归地定义最优解的值。
- 计算最优解的值,通常采用自底向上的方法。
- (回溯)利用计算出的信息构造一个最优解。——如果仅仅需要一个最优解的值,而非解本身,则不需要此步骤
15.1 钢条切割
对于长度为\(n\)英寸的钢条,共有\(2^{n - 1}\)种不同的切割方案。
对于切割长度为\(n\)英寸的钢条的最优收益\(r_n(n \geqslant 1)\),我们可以用更短的钢条的最优切割收益来描述:
\(r_n = max(p_n, r_1 + r_{n-1}, …, r_{n-1} + r_1)\)
第一个参数\(p_n\)对应不切割直接出售长度为\(n\)英寸的钢条的方案,其他\(n - 1\)个参数对应另外\(n - 1\)种方案:对每个\(i = 1, 2, …, n - 1\),首先将钢条切割为长度为\(i\)和\(n - i\)的两段,接着求解这两段的最优切割收益\(r_i\)和\(r_{n-i}\)——此时我们必须考察所有可能的\(i\),选取其中最大收益者。
注意到,为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。我们称钢条切割问题满足最优子结构(optimal substructure)性质:问题的最优解由相关问题的最优解组合而成,而这些子问题可以独立求解。
我们可以将问题分解的方式简化为:将长度为\(n\)的钢条分解为左边开始一段,以及剩余部分继续分解的结果,得到简化后的公式如下(在此公式中,原问题的最优解只包含一个相关子问题的解。):
\(r_n = max_{1 \leqslant i \leqslant n}(p_i, r_{n-i})\)
自顶向下递归实现
CUT-ROD(p, n)
1 if n == 0
2 return 0
3 q = -∞
4 for i = 1 to n
5 q = max(q, p[i] + CUT-ROD(p, n - i))
6 return q
此时,一旦输入规模稍微变大,程序运行时间会变得相当长。原因在于CUT-ROD反复地用相同的参数值对自身进行递归调用——它反复求解相同的子问题。
使用动态规划方法求解最优钢条切割问题
动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。
动态规划付出额外的内存空间来节省计算时间,是典型的时空权衡(time-memory trade-off)的例子。
动态规划有两种等价的实现方法:
- 带备忘的自顶向下法(top-down with memoization)。此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否以及保存过此解,如果是,则之间返回保存的值,从而节省了计算时间——我们称这个递归过程是带备忘的(memoized)。
MEMOIZED-CUT-ROD(p, n)
1 let r[0..n] be a new array
2 for i = 0 to n
3 r[i] = -∞
4 return MEMOIZED-CUT-ROD-AUX(p, n, r)
MEMOIZED-CUT-ROD-AUX(p, n, r)
1 if r[n] >= 0
2 return r[n]
3 if n == 0
4 q = 0
5 else q = -∞
6 for i = 1 to n
7 q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n - i, r))
8 r[n] = q
9 return q
- 自底向上法(bottom-up method)。这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解——将子问题按规模排序,按由小到大的顺序进行求解,当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存——每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。
BOTTOM-UP-CUT-ROD(p, n)
1 let r[0..n] be a new array
2 r[o] = 0
3 for j = 1 to n
4 q = -∞
5 for i = 1 to j
6 q = max(q, p[i] + r[j - i])
7 r[j] = q
8 return r[n]
两种方法得到的算法具有相同的渐近运行时间(由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂性函数通常具有更小的系数)。仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。
子问题图
重构解
15.2 矩阵链乘法
矩阵链乘法问题(matrix-chain multiplication problem)可描述如下:给定\(n\)个矩阵的链\(< A_1, A_2, …, A_n >\),矩阵\(A_i\)的规模为\(p_{i-1} × p_i(1 \leqslant i \leqslant n)\),求完全括号化方案,使得计算乘积\(A_1A_2…A_n\)所需标量乘法次数最少。
计算括号化方案的数量
应用动态规划方法
步骤1:最优括号化方案的结构特征
步骤2:一个递归求解方案
\(m[i, j] = 0——(i = j)\)
\(m[i, j] = \min_{i \leqslant k < j}{m[i, k] + m[k + 1, j] + p_{i-1}p_kp_j}——(i < j)\)