算法设计与分析——动态规划(四):动态规划详解

分类目录:《算法设计与分析》总目录
相关文章:
· 动态规划(一):基础知识
· 动态规划(二):钢条切割
· 动态规划(三):矩阵链乘法
· 动态规划(四):动态规划详解
· 动态规划(五):最长公共子序列


虽然我们已经用动态规划方法解决了钢条切割矩阵链乘法两个问题,但你可能还是弄不清应该在何时使用动态规划。我们关注适合应用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构子问题重叠。我们还会再次讨论备忘方法,更深入地讨论在自顶向下方法中如何借助备忘机制来充分利用子问题重叠特性。

最优子结构

用动态规划方法求解最优化问题的第一步就是刻画最优解的结构。如前文所述,如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个好线索。使用动态规划方法时,我们用子问题的最优解来构造原问题的最优解。因此,我们必须小心确保考察了最优解中用到的所有子问题。

之前的文章介绍的两个问题都具有最优子结构性质。在钢条切割问题中,我们观察到,长度为 n n n的钢条的最优切割方案是由第一次切割后(如果最优切割方案需要进行切割)得到的两段钢条的最优切割方案组成的。在矩阵链乘法问题中,我们看到 A 1 A 2 ⋯ A n A_1A_2\cdots A_n A1A2An的最优括号化方案首先在 A k A_k Ak A k + 1 A_{k+1} Ak+1之间进行划分,然后对 A 1 A 2 ⋯ A k A_1A_2 \cdots A_k A1A2Ak A k + 1 A k + 2 ⋯ A n A_{k+1}A_{k+2} \cdots A_n Ak+1Ak+2An,继续进行最优括号化。

你会发现,在发掘最优子结构性质的过程中,实际上遵循了如下的通用模式:

  1. 证明问题最优解的第一个组成部分是做出一个选择,例如,选择钢条第一次切割位置,选择矩阵链的划分位置等。做出这次选择会产生一个或多个待解的子问题。
  2. 对于一个给定冋题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。
  3. 给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间。
  4. 利用“剪切一粘贴”( cut-and-paste)技术证明:作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点是利用反证法:假定子问题的解不是其自身的最优解,那么我们就可以从原问题的解中“剪切”掉这些非最优解,将最优解“粘贴”进去,从而得到原问题一个更优的解,这与最初的解是原问题最优解的前提假设矛盾。如果原问题的最优解包含多个子问题,通常它们都很相似,我们可以将针对一个子问题的“剪切一粘贴”论证方法稍加修改,用于其他子问题。

一个刻画子问题空间的好经验是:保持子问题空间尽可能简单,只在必要时才扩展它。例如,我们在求解钢条切割问题时,子问题空间中包含的问题为:对每个 i i i值,长度为 i i i的钢条的最优切割问题。这个子问题空间很有效,因此我们不必尝试更一般性(从而也更大)的子问题空间。

与之相对的,假定我们试图限制矩阵链 A 1 A 2 ⋯ A j A_1A_2 \cdots A_j A1A2Aj乘法问题的子问题空间。如前所述,最优括号化方案必然在某个位置 k ( 1 ≤ k < j ) k(1\leq k<j) k(1k<j)处,即 A k A_k Ak A k + 1 A_{k+1} Ak+1之间对矩阵链进行划分。除非我们能保证 k k k永远等于 j − 1 j-1 j1,否则我们会发现得到两个形如 A 1 A 2 ⋯ A k A_1A_2 \cdots A_k A1A2Ak A k + 1 A k + 2 ⋯ A j A_{k+1}A_{k+2} \cdots A_j Ak+1Ak+2Aj的子问题,而后者的形式与 A 1 A 2 ⋯ A j A_1A_2 \cdots A_j A1A2Aj是不同的。因此,对矩阵链乘法问题,我们必须允许子问题在“两端”都可以变化,即允许子问题 A i A i + 1 A i + 2 ⋯ A j A_iA_{i+1}A_{i+2} \cdots A_j AiAi+1Ai+2Aj i i i j j j都可变对于不同问题领域,最优子结构的不同体现在两个方面:

  1. 原问题的最优解中涉及多少个子问题。
  2. 在确定最优解使用哪些子问题时,我们需要考察多少种选择。

在钢条切割问题中,长度为 n n n的钢条的最优切割方案仅仅使用一个子问题(长度为 n − i n-i ni的钢条的最优切割),但我们必须考察 i i i n n n种不同取值,来确定哪一个会产生最优解。 A i A i + 1 A i + 2 ⋯ A j A_iA_{i+1}A_{i+2} \cdots A_j AiAi+1Ai+2Aj的矩阵链乘法问题中,最优解使用两个子问题,我们需要考察 j − i j-i ji种情况。对于给定的矩阵链划分位置 A k A_k Ak,我们需要求解两个子问题 A i A i + 1 ⋯ A k A_iA_{i+1} \cdots A_k AiAi+1Ak A k + 1 A k + 2 ⋯ A j A_{k+1}A_{k+2} \cdots A_j Ak+1Ak+2Aj的括号化方案而且两个子问题都必须求解最优方案。一旦我们确定了子问题的最优解,就可以在 j − i j-i ji个候选的 k k k中选取最优者。

我们可以用子问题的总数和每个子问题需要考察多少种选择这两个因素的乘积来粗略分析动态规划算法的运行时间。对于钢条切割问题,共有 n n n个子问题,每个子问题最多需要考察 n n n种选择,因此运行时间为 O ( n 2 ) O(n^2) O(n2)。矩阵链乘法问题共有 n 2 n^2 n2个子问题,每个子问题最多需要考察 n − 1 n-1 n1种选择,因此运行时间为 O ( n 3 ) O(n^3) O(n3)

子问题图也可用来做同样的分析。图中每个顶点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。回忆一下,钢条切割问题的子问题图有 n n n个顶点,每个顶点最多 n n n条边,因此运行时间为 O ( n 2 ) O(n^2) O(n2)。对于矩阵链乘法问题,子问题图会有 n 2 n^2 n2个顶点,而每个顶点最多有 n − 1 n-1 n1条边,因此共有 O ( n 3 ) O(n^3) O(n3)个顶点和边。

在动态规划方法中,我们通常自底向上地使用最优子结构。也就是说,首先求得子问题的最优解,然后求原问题的最优解。在求解原问题过程中,我们需要在涉及的子问题中做出选择,选出能得到原问题最优解的子问题。原问题最优解的代价通常就是子问题最优解的代价再加上由此次选择直接产生的代价。例如,对于钢条切割问题,我们首先求解子问题,确定长度为 i = 0 , 1 , 2 , ⋯   , n i=0, 1, 2,\cdots, n i=0,1,2,,n的钢条的最优切割方案,然后再确定哪个子问题的解构成长度为 n n n的钢条的最优切割方案。在矩阵链乘法问题中,我们先确定子矩阵链 A i A i + 1 A i + 2 ⋯ A j A_iA_{i+1}A_{i+2} \cdots A_j AiAi+1Ai+2Aj的最优括号化方案,然后选择划分位置 A k A_k Ak,选择本身所产生的代价就是 p i − 1 p k p j p_{i-1}p_kp_j pi1pkpj

在第后面的文章中,我们将介绍“贪心算法”,它与动态规划有很多相似之处。特别是,能够应用贪心算法的问题也必须具有最优子结构性质。贪心算法和动态规划最大的不同在于,它并不是首先寻找子问题的最优解,然后在其中进行选择,而是首先做出一次“贪心”选择——在当时(局部)看来最优的选择——然后求解选出的子问题,从而不必费心求解所有可能相关的子问题。令人惊讶的是,在某些情况下这一策略也能得到最优解!

在尝试使用动态规划方法时要小心,要注意问题是否具有最优子结构性质。考虑下面两个问题,其中都是给定一个有向图 G = ( V , E ) G=(V, E) G=(V,E)和两个顶点 u , v ∈ V u,v∈V u,vV

子问题无关
  • 无权最短路径:找到一条从 u u u v v v的边数最少的路径。这条路径必然是简单路径,因为如果路径中包含环,将环去掉显然会减少边的数量。
  • 无权最长路径:找到一条从 u u u v v v的边数最多的简单路径。这里必须加上简单路径的要求,因为我们可以不停地沿着环走,从而得到任意长的路径。

下面我们证明无权最短路径问题具有最优子结构性质。假设 u ≠ v u≠v u=v,则问题是非平凡的。这样,从 u u u v v v的任意路径 p p p都必须包含一个中间顶点,比如 w w w(注意, w w w可能是 u u u v v v)。因此,我们可以将路径 u → v u→v uv分解为两条子路径 u → w u→w uw w → v w→v wv。显然, p p p的边数等于 p u w p_{uw} puw的边数加上 p w v p_{wv} pwv的边数。于是,我们断言:如果 p p p是从 u u u v v v的最最短路径,那么 p u w p_{uw} puw必须是从 u u u w w w的最短路径我们可以用“剪切一粘贴”方法来证明:如果存在另一条从 u u u w w w的路径 p u w ′ p_{uw}' puw,其边数比 p u w p_{uw} puw少,那么可以剪切掉 p u w p_{uw} puw,将 p u w ′ p_{uw}' puw粘贴上,构造出一条比 p p p边数更少的路径,与 p p p最优的假设矛盾。

你可能已经倾向于假设无权最长简单路径问题也具有最优子结构性质。毕竟,如果我们将最长简单路径 u → v u→v uv分解为子路径 u → w u→w uw w → v w→v wv,难道 p u w p_{uw} puw不应该是从 u u u w w w的最长简单路径, p u w p_{uw} puw不应该是从 w w w v v v的最长简单路径吗?但答案是否定的!
无权最长路径
考虑路径 q → r → t q→r→t qrt,它是从 q q q t t t的最长简单路径。但 q → r q→r qr并是从 q q q r r r的最长简单路径, q → s → t → r q→s→t→r qstr是一条更长的简单路径。这个例子说明,最长简单路径问题不仅缺乏最优子结构性质,由子问题的解组合出的甚至都不是原问题的“合法”解。

的确,无权最长简单路径问题看起来不像有任何形式的最优子结构。对此问题尚未找到有效的动态规划算法。实际上,此问题是NP完全的,我们在后面的文章中将会看到,这意味着我们不太可能找到多项式时间的求解方法。

最长简单路径问题的子结构与最短路径有这么大的差别的原因在于,虽然最长路径问题和最短路径问题的解都用到了两个子问题,但两个最长简单路径子问题是相关的,而两个最短路径子问题是无关的。这里,子问题无关的含义是,同一个原问题的一个子问题的解不影响另一个子问题的解。还是上图的例子,求 q q q t t t的最长简单路径可以分解为两个子问题:求 q q q r r r的最长简单路径和 r r r t t t的最长简单路径。对于前者,我们选择路径 q → t → r q→t→r qtr,其中用到了顶点 s s s t t t。由于两个子问题的解的组合必须产生一条简单路径,因此我们在求解第二个子问题时就不能再用这两个顶点了。但如果在求解第二个子问题时不允许使用顶点 t t t,就根本无法进行下去了,因为 t t t是原问题解的路径终点,是必须用到的,还不像子问题解的“接合”顶点 r r r那样可以不用。这样,由于一个子问题的解使用了顶点 s s s t t t,在另一个子问题的解中就不能再使用它们,但其中至少一个顶点在求解第二个子问题时又必须用到,而获得最优解则两个都要用到。因此,我们说两个子问题是相关的。换个角度来看,我们所面临的困境就是:求解一个子问题时用到了某些资源,导致这些资源在求解其他子问题时不可用。

钢条切割矩阵链乘法讨论的两个问题都具有子问题无关性质。在矩阵链乘法问题中,子问题为子链 A i A i + 1 ⋯ A k A_iA_{i+1} \cdots A_k AiAi+1Ak A k + 1 A k + 2 ⋯ A j A_{k+1}A_{k+2} \cdots A_j Ak+1Ak+2Aj的乘法问题。子链是互不相交的,因此任何矩阵都不会同时包含在两条子链中。在钢条切割问题中,为了确定长度为 n n n的钢条的最优切割方案,我们考察所有长度为 i i i的钢条的最优切割方案。由于长度为 n n n的问题的最优解只包含一个子问题的解,子问题无关性显然是可以保证的。

重叠子结构

适合用动态规划方法求解的最优化问题应该具备的第二个性质是子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。一般来讲,不同子问题的总数是输入规模的多项式函数为好。如果递归算法反复求解相同的子问题,我们就称最优化问题具有重叠子问题性质°。与之相对的,适合用分治方法求解的问题通常在递归的每一步都生成全新的子问题。动态规划算法中通常对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题时直接查表,每次查表的代价为常量时间。

《算法设计与分析——动态规划(二):钢条切割》中,我们简单分析了钢条切割问题的递归算法是如何通过指数次的递归调用来求解小的子问题。而我们的动态规划算法将运行时间从递归算法的指数阶降为平方阶。

重构最优解

从实际考虑,我们通常将每个子问题所做的选择存在一个表中,这样就不必根据代价值来重构这些信息。

对矩阵链乘法问题,利用表 s [ i , j ] s[i, j] s[i,j],我们重构最优解时可以节省很多时间。假定我们没有维护 s [ i , j ] s[i, j] s[i,j]表,只是在表 m [ i , j ] m[i, j] m[i,j]中记录了子问题的最优代价。当我们确定 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj的最优括号化方案用到了哪些子问题时,就需要检查所有 j − i j-i ji种可能,而 j − i j-i ji并不是一个常数。因此,对一个给定问题的最优解,重构它用到了哪些子问题就需花费e(j-i)=a(1)的时间。

备忘

如我们在在《算法设计与分析——动态规划(二):钢条切割》中中所见,我们可以保持自顶向下策略,同时达到与自底向上动态规划方法相似的效率。思路就是对自然但低效的递归算法加入备忘机制。与自底向上方法一样,我们维护一个表记录子问题的解,但仍保持递归算法的控制流程。

带备忘的递归算法为每个子问题维护一个表项来保存它的解。每个表项的初值设为一个特殊值,表示尚未填人子问题的解。当递归调用过程中第一次遇到子问题时,计算其解,并存入对应表项。随后每次遇到同一个子问题,只是简单地查表,返回其解。

通常情况下,如果每个子问题都必须至少求解一次,自底向上动态规划算法会比自顶向下备忘算法快(都是 O ( n ) O(n) O(n)时间,相差一个常量系数),因为自底向上算法没有递归调用的开销,表的维护开销也更小。而且,对于某些问题,我们可以利用表的访问模式来进一步降低时空代价。相反,如果子问题空间中的某些子问题完全不必求解,备忘方法就会体现出优势了,因为它只会求解那些绝对必要的子问题。

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值