本节关注在什么情况下适用动态规划方法。适合用动态规划方法求解的问题应该具备两个要素:最优子结构和子问题重叠。
1.最优子结构
以两个例子来说明如何判断一个问题是否具有最优子结构。给定一个有向图G = (V, E)和两个顶点u, v ∈ V。考虑下面两个问题。
(1) 无权最短路径:找到一条从u到v的边数最少的路径。这条路径必然是简单路径,因为如果路径中包含环,将环去掉显然可以减少边数。
无权最短路径问题具有最优子结构。假设u ≠ v。这样,从u到v的任意路径p必然包含一个中间顶点,比如w(注意,这里的w可能是u或v本身)。于是,我们可以将路径分解为两条子路径 和 。路径p的边数等于路径的边数加上路径的边数。
可以断言:如果p是从u到v的最短路径,那么必须是从u到w的最短路径,并且也必须是从w到v的最短路径。因为如果从u到w存在比更短的路径,那么可以用这条更短的路径替换掉,从而得到一个更优解,所以必然是从u到w的最短路径。必然是从w到v的最短路径也是同样的道理。因此,无权最短路径问题满足最优子结构。
(2) 无权最长路径:找到一条从u到v的边数最多的简单路径。这里必须加上简单路径的要求,因为如果路径中存在环,可以绕这个环走任意的圈数,从而得到一条任意长的路径。
无权最长路径不具备最优子结构。下图给出了一个反例。考虑路径q→r→t,它是从q到t的最长简单路径。但是子路径q→r并不是从q到r的最长简单路径,q→s→t→r才是。同样,子路径r→t并不是从r到t的最长简单路径,r→q→s→t才是。因此,无权最长路径问题不具备最优子结构。
我们探讨一下最长简单路径问题不具备最优子结构的根本原因。虽然最长简单路径问题也可以划分出两个子问题,但是这两个子问题是相关的。在上面的例子中,子问题“q到r的最长简单路径”的最优解为q→s→t→r,而另一个子问题“r到t的最长简单路径”的最优解为r→q→s→t。可以看到,两个子问题的最优路径中出现了相同的顶点,这违背了简单路径的条件限制。简单路径的条件要求:如果一个顶点t在一个子问题的最优解中出现,那么在求解另一个子问题时不能用到这个顶点。因此,两个子问题并不互相独立。用更抽象的语言来描述,就是“求解一个子问题时用到了某些资源,导致这些资源在求解其他子问题时不可用”。
现在回过头来看最短简单路径问题,也举一个例子,如下图所示。显然,u到v的最短简单路径是u→w→v。我们在求解过程中,会依次考察以w和x作为中间顶点的情况。如果以x作为中间顶点,子问题u到x的最短简单路径是u→w→x,子问题x到v的最短简单路径是x→w→v,于是得到u到v的路径为u→w→x→w→v。可以看到,这并不是一条简单路径,并且两个子问题的路径中出现了相同的顶点w。因此,以x作为中间顶点的情况,两个子问题也不是互相独立的,但这并不是问题的最优解。最优解是以w作为中间结点的情况,在这种情况下,两个子问题是互相独立的。
只要最优解满足子问题互相独立的条件,即使求解过程中遇到的一些非最优解存在子问题不相互独立的情况,我们也可以采用动态规划方法来求解。在最短简单路径问题中,因为最优解必然是一条简路径,故而最优解的子问题一定是互相独立的。
2.子问题重叠
如果一个问题具备最优子结构,并且求解过程中需要反复地求解相同的子问题,采用动态规划方法才有意义。动态规划的一个特点是,为节省算法运行时间,每个子问题只求解一次,并且将子问题的解保存下来。动态规划的两种求解方式都体现了这一特点。
(1) 按照规模由小到大的顺序求解子问题。在求解一个规模较大的子问题时,它所依赖的规模较小的子问题都已经求解了,并且这些规模较小的子问题的解已经被保存下来,可以直接引用。
(2) 带备忘的递归方式。按照从上到下的递归方式求解问题。如果在递归过程中遇到一个新的子问题,直接求解它,并将它的解保存起来;如果遇到一个旧的子问题,它已经被求解过,直接引用它的解。
15.3-1 对于矩阵链乘法问题,下面两种确定最优代价的方法哪种更高效?第一种方法是穷举所有可能的括号化方案,对每种方案计算乘法运算次数;第二种方法是运行RECURSIVE-MATRIX-CHAIN。证明你的结论。(下面给出了方法二的伪代码。)
解
方法一是穷举所有可能的括号化方案,书本中给出了它的运行时间的下限是,这里不做证明。
现在我们求解方法二的运行时间。方法二是运行RECURSIVE-MATRIX-CHAIN。根据代码一,可以写出它的运行时间的递归式为
O(1)表示运行时间不超过常数时间,可以用两个正常数a和b分别替代递归式中的两个O(1),并将等式变为不等式,如下所示。
上式中, 实际上可以简化为 。例如
… …
… …
因此,上面的递归式可以写为
通过画递归树,可以推测该递归式的解为。这里不详细描述如何画递归树。下面用代入法证明。
用代入法,我们可能会很自然地假设,其中c是一个正常数。但是我们如果用这个假设去推导,会发现根本无法得到我们想要的结果。我们需要修改一下这个假设,减去一个常数项,即,其中s是一个正常数。
取初始条件,T(1)是个常数时间。显然,只要c足够大,就能满足。
现在考虑n > 1的情况。假设存在c > 0,使得对1 ~ n-1都成立。于是有
为方便起见,假设取s = a/2,那么上式可以写为
只要取足够大的c,就能使 成立,此时 显然是成立的。
综上所述,只要选取合适的s(例如s = a/2),并且选取足够大的c,就能使对所有n > 0都成立。因此成立。
现在比较方法一和方法二的运行时间。方法一的运行时间的下限是,方法二的运行时间的上限是。显然,方法二的运行时间更小。
笔者在网上看到,该习题的大多数解答给出的方法二的运行时间为。究其原因,应该就是在用代入法时,没有做出合适的假设。合适的假设应当减去一个常数项,即。读者如有疑问,可以去复习一下《算法导论》4.3节。
15.3-2 对一个16个元素的数组,画出2.3.1节中MERGE-SORT过程运行的递归调用树。解释备忘技术为什么对MERGE-SORT这种分治算法无效。
解
用(i, j)代表归并排序位置i ~ j的元素。下图为16个元素的递归树。
MERGE-SORT虽然把排序问题划分为子问题,然而子问题并不重叠,所以没有必要采用备忘的方法。
15.3-3 考虑矩阵链乘法问题的一个变形:目标改为最大化矩阵序列括号化方案的标量乘法运算次数,而非最小化。此问题具有最优子结构性质吗?
解
此问题也具有最优子结构性质。用m[i, j]表示计算矩阵链乘法所需要的最大计算代价。假设的代价最大的完全括号化方案在位置k处拆分,那么有m[i, j] = m[i, k] + m[k+1, j] + 。显然,左子链的完全括号化方案必须是最大代价的。因为,如果左子链有一个代价更大的完全括号化方案,那么把它替换到公式m[i, j] = m[i, k] + m[k+1, j] + 里面,会产生一个更优解。同理,右子链的完全括号化方案也必须是代价最大的。并且,左子链与右子链是相互独立的两个子问题。下面给出了该问题的递归式。
15.3-4 如前所述,使用动态规划方法,我们首先求解子问题,然后选择哪些子问题用来构造原问题的最优解。Capulet教授认为,我们不必为了求原问题的最优解而总是求解出所有子问题。她建议,在求矩阵链乘法问题的最优解时,我们总是可以在求解子问题之前选定的划分位置(选定的k使得最小)。请找出一个反例,证明这个贪心方法可能生成次优解。
解
假设要求3个矩阵的乘积,其中,为2×3矩阵,为3×4矩阵,为4×4矩阵。按照Capulet的算法,的计算顺序应当为,这种方案的代价为
(3 × 4 × 4) + (2 × 3 × 4) = 72
然而上面这种方案并不是代价最小的。因为如果计算顺序为,才会得到最小代价
(2 × 3 × 4) + (2 × 4 × 4) = 56
在求解时,虽然从处划分所得到的局部代价2 × 3 × 4 = 24比从处划分所得到的局部代价2 × 4 × 4 = 32要小,然而子问题的代价3 × 4 × 4 = 48比子问题的代价2 × 3 × 4 = 24要大,综合起来看,从处划分虽然局部最优,但并不是问题的全局最优解。
15.3-5 在15.1节的钢条切割问题中,加入限制条件:切割一段长钢条,得到的长度为i的短钢条的数目不能超过,i = 1, 2, … , n-1。也就是对每种长度的短钢条的数目作一个限制。证明:15.1节所描述的最优子结构性质不再成立。(算法导论中文版翻译不太好,意思不明确,笔者对本题题目做了一个重新表述。)
解
15.1节的最优子结构,其递归式如下所示。
我们现在直接套用这个递归式来分析这个问题。切割一段长度为n的钢条,假设切割下来的第一段长度为i。在求解子问题时,也就是在切割长度为n-i的钢条时,假设切割下来的长度为i的短钢条的数目刚好为最大值,那么在整段钢条的切割方案中,长度为i的短钢条有个,这超过了的最大值限制。因此,直接套用这个递归式,可能会得到一个不符合限制条件的解。
从另一个角度分析,在求解子问题时,也就是在切割长度为n-i的钢条时,切割下来的长度为i的短钢条的数目不能超过,减1是因为求解子问题之前已经切下来一段长度为i的短钢条了。因此,子问题相对原问题来说,不光是问题规模变小了,限制条件也发生了改变。这带来一个问题,即使求解相同规模的子问题,会因为限制条件不同而产生不同的结果,也就是说相同规模的子问题可能会因为限制条件的变化而需要多次求解。这不符合最优子结构“相同的子问题只需要求解一次”的原则。
15.3-6 假定你希望兑换外汇,你意识到与其直接兑换,不如进行多种外币的一系列兑换,最后兑换到你想要的那种外币,可能会获得更大收益。假定你可以交易n种不同的货币,编号为1, 2, … , n。兑换从1号货币开始,总终兑换为n号货币。对每两种货币i和j,给定汇率,意味着你如果有d个单位的货币i,可以兑换个单位的货币j。进行一系列的交易需要支付一定的佣金,金额取决于交易的次数。令表示k次交易需要支付的佣金。证明:如果对所有k = 1, 2, … , n, = 0,那么寻找最优兑换序列的问题具有最优子结构性质。然后请证明:如果佣金为任意值,那么问题不一定具有最优子结构性质。
该题笔者无法解答,希望有高手来指点。