前言

和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的。分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题。在这种情况下,分治法会重复地求解公共的子子问题。而动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重复。
动态规划通常用于最优化问题 。此类问题可能有多种可行解。每个解有一个值,而我们希望找出具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因而可能存在多个最优解。
动态规划算法的设计可以分为如下4个步骤:

  1. 描述最优解的结构
  2. 递归定义最优解的值
  3. 按自底向上的方式计算最优解的值
  4. 由计算出的结构构造一个最优解

钢条切割

塞林企业会买进长钢条,将它们切割成短条后卖出(切割是免费的,不计成本)。塞林企业的老总想要知道钢条怎么切割最赚钱。
已知塞林企业对长度为 i 英寸的钢条的售价为 pi 美元,其中 i = 1,2,…。下图给出了一张样本价格表。

一根长度为n的钢条,每个切口都有切和不切的选择,即 共有2n−12^{n-1}2n−1 种不同的切割方式。我们可以将最优收益 rnr_nrn​ 表示成如下形式:
rn=max(pn,r1+rn−1,r2+rn−2,…,rn−1+r1)r_n = max ( p_n , r_1 + r_{n-1} , r_2 + r_{n-2} ,…, r_{n-1} + r_1 )rn​=max(pn​,r1​+rn−1​,r2​+rn−2​,…,rn−1​+r1​)
第一个参数 pnp_npn​ 代表不切割时钢条的价格。其它的 n - 1个参数首先将钢条分为2份,长度分别为 i 和 n - i ( i = 1,2,…, n - 1),然后分别取得两份的最优收益 rir_iri​ 和 rn−ir_{n-i}rn−i​ 之后做和。再把pn=r0+rn−0p_n = r_0 + r_{n-0}pn​=r0​+rn−0​得出一种更简单的递归结构
rn=max(ri+rn−i)r_n = max ( r_i + r_{n-i} )rn​=max(ri​+rn−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 的效率如此低下的原因就是它不断的重复解决相同的子问题。下图给出了一个很好的说明,其中 n = 4,可以看到,过程多次重复计算 n = 2和 n = 1。

使用动态规划解决钢条切割问题

动态规划会仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来以便之后查找。由此可见,动态规划需要额外的内存空间来节省计算时间,是典型的 时空权衡 (time-memory trade-off)的例子。

带备忘的自顶向下法(top-down with memoization)

此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解。

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      reutrn r[n]
3  if n == 0
4      q = 0
5  else
6      q = -∞
7      for i = 1 to n
8          q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n - i, r))
9  r[n] = q
10 return q
自底向上法(bottom-up method)

这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小”子问题的求解。因而我们可以将子问题按规模排序,由小到大一次求解。当求解某子问题时,它所依赖的那些更小子问题都已求解完毕,因此每个子问题只求解一次。

BOTTOM-UP-CUT-ROD(p, n)
1 let r[0..n] be a new array
2 r[0] = 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]

子问题图

当思考一个动态规划为问题时,我们应该了解问题的子问题之间的依赖关系。问题的子问题图准确地表达了这些信息,子问题图是一个有向图,每个定点唯一地对应一个子问题。如果求子问题 x 的最优解时需要直接用到子问题 y 的最优解,那么在子问题图中就会有一条从子问题 x 到子问题 y 的有向边。下图显示了 n = 4时钢条切割问题的子问题图。

子问题图 G = ( V , E )的规模可以帮助我们确定动态规划的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度成正比,而子问题的数目等于子问题的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系

重构解

上面的算法仅返回最优解的收益值rnr_nrn​,并未返回如何切割

EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
1  let r[0..n] and s[0..n] be new arrays
2  r[0] = 0
3  for j = 1 to n
4      q = -∞
5      for i = 1 to j
6          if q < p[i] + r[j - i]
7              q = p[i] + r[j - i]
8              s[j] = i
9      r[j] = q
10 return r and s

练习

1-1由公式(15.3)和初始条件T(0)=1,证明公式(15.4)成立。

(略)

1-2 举反例证明下面的“贪心”策略不能保证总是得到最优切割方案。定义长度为i的钢条的密度pi/i,即每英寸的价值。贪心策略将长度为n的钢条切割下长度为i(1<i<n)的一段,其密度最高。接下来继续使用相同的策略切割长度为n-i的剩余部分。

1-3 我们对钢条切割问题进行一点修改,除了切割下的钢条段具有不同价格pi外,每次切割还要付出固定的成本c.这样,切割方案的收益就等于钢条段价格之和减去切割的成本。设计一个动态规划算法解决修改后的钢条切割问题。

MODIFIED-CUT-ROD(p, n, c)let r[0..n] be a new arrayr[0] = 0for j = 1 to nq = p[j]for i = 1 to j - 1q = max(q, p[i] + r[j - i] - c)r[j] = qreturn r[n]

1-4 修改MEMOIZED-CUT-ROD,使之不仅返回最优收益值,还返回切割方案。

MEMOIZED-CUT-ROD(p, n)let r[0..n] and s[0..n] be new arraysfor i = 0 to nr[i] = -∞(val, s) = MEMOIZED-CUT-ROD-AUX(p, n, r, s)print "The optimal value is" val "and the cuts are at" sj = nwhile j > 0print s[j]j = j - s[j]MEMOIZED-CUT-ROD-AUX(p, n, r, s)if r[n] ≥ 0return r[n]if n == 0q = 0else q = -∞for i = 1 to n(val, s) = MEMOIZED-CUT-ROD-AUX(p, n - i, r, s)if q < p[i] + valq = p[i] + vals[n] = ir[n] = qreturn (q, s)
1-5斐波那契额数列可以用递归式F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2).定义。设计一个O(n)时间的动态规划算法计算第n个斐波那契数。画出子问题图。图中有多少顶点和边?
FIBONACCI(n)let fib[0..n] be a new arrayfib[0] = 1fib[1] = 1for i = 2 to nfib[i] = fib[i - 1] + fib[i - 2]return fib[n]


以上是n=5的子问题图,可以看出有6个顶点和8条边,推广到n情况,有n+1个顶点,有2n-2条边。

矩阵链乘法

给定n个矩阵构成的一个链<A1,A2,A3,.......An><A_1,A_2,A_3,.......A_n><A1​,A2​,A3​,.......An​>,其中i=1,2,…n,矩阵A的维数为pi−1∗pip_{i-1}* p_ipi−1​∗pi​,求完全的括号化方案,使得计算乘积 A1A2...AnA_1A_2...A_nA1​A2​...An​ 所需标量乘法最少。

括号方案:

  • 比如矩阵A是p x q大小,矩阵B是q x r大小,很明显,得到的矩阵C是p x r大小,其中花费的时间必定是p*q*r
  • 比如A(p x q), B(q x r), C(r x l)这三个矩阵相乘。如果不规划,那么花费的时间是AB=pqr,然后再乘以C,还需要额外花费prl时间。但有可根据矩阵乘法结合律BC先乘,然后再乘以A,这样花费的时间最少。

令P(n)表示n个矩阵的矩阵链的所有加括号的方案的数量。即公式如下

显然,遍历所有加括号的方案,并不是一个明智的选择,这样的算法至少有一个指数增长的时间复杂度。现在我们用动态规划方法来求解这个问题。

步骤1:最优括号化方案的结构特征

假设现在要计算AiAi+1…Aj的值,计算Ai…j过程当中肯定会存在某个k值(i<=k<j)将Ai…j分成两部分,使得Ai…j的计算量最小。分成两个子问题Ai…k和Ak+1…j,需要继续递归寻找这两个子问题的最优解。

步骤2:一个递归求解方案

设m[i,j]为计算机矩阵Ai…j所需的标量乘法运算次数的最小值,对此计算A1…n的最小代价就是m[1,n]。现在需要来递归定义m[i,j],分两种情况进行讨论如下

步骤3:计算最优代价

虽然给出了递归解的过程,但是在实现的时候不采用递归实现,而是借助辅助空间,使用自底向上的表格进行实现。设矩阵Ai的维数为pi-1pi,i=1,2…n。输入序列为:p=<p0,p1,…pn>,length[p] = n+1。使用m[n][n]保存m[i,j]的代价,s[n][n]保存计算m[i,j]时取得最优代价处k的值,最后可以用s中的记录构造一个最优解。

MAXTRIX_CHAIN_ORDER(p)2   n = length[p]-1;3   for i=1 to n4       do m[i][i] = 0;5   for t = 2 to n  //t is the chain length6        do for i=1 to n-t+17                      j=i+t-1;8                      m[i][j] = MAXLIMIT;9                      for k=i to j-1
10                             q = m[i][k] + m[k+1][i] + qi-1qkqj;
11                             if q < m[i][j]
12                                then m[i][j] = q;
13                                     s[i][j] = k;
14   return m and s;

步骤4:构造最优解

第三步中已经计算出来最小代价,并保存了相关的记录信息。因此只需对s表格进行递归调用展开既可以得到一个最优解。

PRINT_OPTIMAL_PARENS(s,i,j)if i== j then print "Ai"elseprint "(";PRINT_OPTIMAL_PARENS(s,i,s[i][j]);PRINT_OPTIMAL_PARENS(s,s[i][j]+1,j);print")";

练习

2-1 对矩阵规模序列{5,10,3,12,5,50,6},求矩阵链最优括号化方案。

((5×10)(10×3))(((3×12)(12×5))((5×50)(50×6))).

2-2设计递归算法MATRIX-CHAIN-MULTIPLY(A,s,i,j),实现矩阵链最优代价乘法计算的真正计算过程,其输入参数为矩阵序列{A1,A2,…,An},MATRIX-CHAIN-ORDER得到的表s,以及下标i和j.(初始调用应为MATRIX-CHAIN-MULTIPLY(A,s,1,n)).

MATRIX-CHAIN-MULTIPLY(A, s, i, j)if i == jreturn A[i]if i + 1 == jreturn A[i] * A[j]b = MATRIX-CHAIN-MULTIPLY(A, s, i, s[i, j])c = MATRIX-CHAIN-MULTIPLY(A, s, s[i, j] + 1, j)return b * c

2-[3-6]

(略)

动态规划原理

动态规划方法求解的问题应该具备两个要素:最优子结构和子问题重叠。

  • 最优解的子问题一定是互相独立的
  • 按照规模由小到大的顺序求解子问题
  • 带备忘的递归方式

练习

3-1对于矩阵链乘法问题,下面两种确定最优代价的方法哪种更高效?第一种方法是穷举所有可能的括号化方案,对每种方案计算乘法运算次数,第二种方法是运行RECURSIVE-MATRIX-CHAIN。证明你的结论。

书中对枚举法已经给出其运行时间,就是类似卡塔兰数的序列,运算次数为Ω(4n/n(3/2))。而用朴素递归方式求全部解的运算次数为O(n3^n).显然用递归方式求解更高效。

3-2 对一个16个元素的数组,画出2,.3-1节中MERGE-SORT过程运行的递归调用树。解释备忘技术为什么对MERGE-SORT这种分治算法无效。


每个子问题都是全新的,不存在重叠子问题。

3-3 考虑矩阵链乘法问题的一个变形,目标改为最大化矩阵序列括号花方案的变量乘法运算次数,而非最小化。此问题具有最优子结构性质吗?

有。

3-4 如前所述,使用动态规划方法,我们首先求解子问题,然后选择哪些子问题用来构造原问题的最优解。Capulet教授认为,我们不必为了求原问题的最优解而总是求解出所有子问题。她建议,在求矩阵链乘法问题的最优解时,我们总是可以在求解子问题之前选定AiAi+1…Aj的划分位置Ak(选定的k使得pi-1pkpj最小)。请找出一个反例,证明这个贪心方法可能生成次优解

假设要求3个矩阵的乘积A1A2A3,其中,A1为2×3矩阵,A2为3×4矩阵,A3为4×4矩阵。按照Capulet的算法,A1A2A3的计算顺序应当为(A1(A2A3)),这种方案的代价为

(3 × 4 × 4) + (2 × 3 × 4) = 72

然而上面这种方案并不是代价最小的。因为如果计算顺序为((A1A2)A3),才会得到最小代价

(2 × 3 × 4) + (2 × 4 × 4) = 56

3-5 在15.1节的钢条切割问题中,加入限制条件:切割一段长钢条,得到的长度为i的短钢条的数目不能超过,i = 1, 2, … , n-1。也就是对每种长度的短钢条的数目作一个限制。证明:15.1节所描述的最优子结构性质不再成立。(算法导论中文版翻译不太好,意思不明确,笔者对本题题目做了一个重新表述。)

不符合最优子结构“相同的子问题只需要求解一次”的原则

3-6

(略)

最长公共子序列

某给定序列的子序列,就是将给定序列中零个或多个元素去掉后得到的结果。其形式化定义如下:给定一个序列 X=<x1,x2,…,xm>X = < x_1 , x_2 , … , x_m >X=<x1​,x2​,…,xm​>,另一个序列 Z=<z1,z2,…,zk>Z = < z_1 , z_2 , … , z_k >Z=<z1​,z2​,…,zk​>,如果 Z 满足如下条件则称 Z 为 X 的 子序列 (subsequence),即存在一个严格递增的 X 的下标序列<i1,i2,…,ik>< i_1 , i_2 , … , i_k ><i1​,i2​,…,ik​>,对所有 j = 1,2,…, k ,满足 xi=zjx_i = z_jxi​=zj​ 。给定两个序列 X 和 Y ,如果 Z 既是 X 的子序列,也是 Y 的子序列,则称它是 X 和 Y 的 公共子序列 。
最长公共子序列问题 (longest-common-subsequence problem)就是给定两个序列 X=<x1,x2,…,xm>X = < x_1 , x_2 , … , x_m >X=<x1​,x2​,…,xm​>和 Y=<y1,y2,…,yn>Y = < y_1 , y_2 , … , y_n >Y=<y1​,y2​,…,yn​>,求 X 和 Y 长度最长的公共子序列。简称LCS问题。下面将展示如何用动态规划方法高效求解LCS问题。

步骤1:描述最长公共子序列的特征

子问题的自然分类对应两个输入序列的“前缀”对。前缀的严格定义如下:给定一个序列 X=<x1,x2,…,xm>X = < x_1 , x_2 , … , x_m >X=<x1​,x2​,…,xm​>,对 i = 0,1,…, m ,定义 X 的第 i 前缀为 Xi=<x1,x2,…,xi>,X0Xi = < x_1 , x_2 , … , x_i >, X_0Xi=<x1​,x2​,…,xi​>,X0​ 为空串。

定理 (LCS的最优子结构)
令 X = < x1 , x2 , … , xm >和 Y = < y1 , y2 , … , yn >为两个序列, Z = < z1 , z2 , … , zk >为 X 和 Y 的任意LCS。

  1. 如果 xm = yn ,则 zk = xm = yn 且 Zk-1 是 Xm-1 和 Yn-1 的一个LCS。
  2. 如果 xm ≠ yn ,那么 zk ≠ xm 意味着 Z 是 Xm-1 和 Y 的一个LCS。
  3. 如果 xm ≠ yn ,那么 zk ≠ yn 意味着 Z 是 X 和 Yn-1 的一个LCS。

上面的定理说明两个序列的LCS包含两个序列的前缀的LCS。因此,LCS问题满足最优子结构性质。

步骤2:一个递归解

设计LCS问题的递归算法还要建立最优解的递归式。令 c [ i , j ]表示 Xi 和 Yj 的LCS的长度。如果 i = 0或 j = 0,即一个序列长度为0,那么LCS的长度为0。根据LCS问题的最优子结构性质,可知:

步骤3:计算LCS的长度

过程 LCS-LENGTH 接受两个序列 X = < x1 , x2 , … , xm >和 Y = < y1 , y2 , … , yn >为输入。它将 c [ i , j ]的值保存在表 c [ 0 … m , 0 … n ],并按 行主次序 (row-major order)计算表项(即首先由左至右计算 c 的第一行,然后第二行,依此类推)。过程还维护一个表 b [ 1 … m , 1 … n ]帮助构造最优解。 b [ i , j ]指向的表项对应计算 c [ i , j ]时所选择的子问题的最优解。过程返回表 b 和表 c , c [ m , n ]保存了 X 和 Y 的LCS的长度。

LCS-LENGTH(X, Y)
1  m = X.length
2  n = Y.length
3  let b[1..m, 1..n] and c[0..m, 0..n] be new tables
4  for i = 1 to n
5      c[i, 0] = 0
6  for j = 0 to n
7      c[0, j] = 0
8  for i = 1 to m
9      for j = 1 to n
10         if x_i == y_j
11             c[i, j] = c[i - 1, j - 1] + 1
12             b[i, j] = "↖"
13         elseif c[i - 1, j] >= c[i, j - 1]
14             c[i, j] = c[i - 1, j]
15             b[i, j] = "↑"
16         else
17             c[i, j] = c[i, j - 1]
18             b[i, j] = "←"
19 return c and b

步骤4:构造LCS

现在可以用 LCS-LENGTH 返回的表 b 快速构造 X = < x1 , x2 , … , xm >和 Y = < y1 , y2 , … , yn >的LCS。

PRINT-LCS(b, X, i, j)
1 if i == 0 or j == 0
2     return
3 if b[i, j] == "↖"
4     PRINT-LCS(b, X, i - 1, j - 1)
5     print x_i
6 elseif b[i, j] == "↑"
7     PRINT-LCS(b, X, i - 1, j)
8 else
9     PRINT-LCS(b, X, i, j - 1)

练习

4-1 求<1,0,0,1,0,1,0,1>和<0,1,0,1,1,0,1,1,0>的一个LCS。

⟨1,0,0,1,1,0⟩or⟨1,0,1,0,1,0⟩.⟨1,0,0,1,1,0⟩ or ⟨1,0,1,0,1,0⟩.⟨1,0,0,1,1,0⟩or⟨1,0,1,0,1,0⟩.

4-2 设计代码,利用完整的表c及原始寻列X={x1,x2,…xm};Y={y1,y2,…yn};来重构LCS,要求运行时间为O(m+n),不能使用表b.

PRINT-LCS(c, X, Y, i, j)if c[i, j] == 0returnif X[i] == Y[j]PRINT-LCS(c, X, Y, i - 1, j - 1)print X[i]else if c[i - 1, j] > c[i, j - 1]PRINT-LCS(c, X, Y, i - 1, j)elsePRINT-LCS(c, X, Y, i, j - 1)

4-3设计LCS-LENGTH的带备忘的版本,运行时间为O(mn);

MEMOIZED-LCS-LENGTH(X, Y, i, j)if c[i, j] > -1return c[i, j]if i == 0 or j == 0return c[i, j] = 0if x[i] == y[j]return c[i, j] = LCS-LENGTH(X, Y, i - 1, j - 1) + 1return c[i, j] = max(LCS-LENGTH(X, Y, i - 1, j), LCS-LENGTH(X, Y, i, j - 1))

4-4 说明如何只使用表c中2 X min(m,n)个表项及O(1)的额外空间来计算LCS的长度。然后说明如何只用min(m,n)个表项及O(1)的额外空间完成相同的工作。

利用c表计算出长度就行

4-5 设计一个O(n²)时间的算法,求一个n个数的序列的最长单调递增子序列。

PRINT-LCS(c, X, Y)n = c[X.length, Y.length]let s[1..n] be a new arrayi = X.lengthj = Y.lengthwhile i > 0 and j > 0if x[i] == y[j]s[n] = x[i]n = n - 1i = i - 1j = j - 1else if c[i - 1, j] ≥ c[i, j - 1]i = i - 1else j = j - 1for i = 1 to s.lengthprint s[i]MEMO-LCS-LENGTH-AUX(X, Y, c, b)m = |X|n = |Y|if c[m, n] != 0 or m == 0 or n == 0returnif x[m] == y[n]b[m, n] = ↖c[m, n] = MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y[1..n - 1], c, b) + 1else if MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y, c, b) ≥ MEMO-LCS-LENGTH-AUX(X, Y[1..n - 1], c, b)b[m, n] = ↑c[m, n] = MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y, c, b)elseb[m, n] = ←c[m, n] = MEMO-LCS-LENGTH-AUX(X, Y[1..n - 1], c, b)MEMO-LCS-LENGTH(X, Y)let c[1..|X|, 1..|Y|] and b[1..|X|, 1..|Y|] be new tablesMEMO-LCS-LENGTH-AUX(X, Y, c, b)return c and b

4-6 设计一个O(nlgn)时间的算法,求一个n个数的序列的最长单调递增子序列。(提示:注意到,一个长度为i的候选子序列的尾元素至少不比一个长度为i-1的候选子序列的尾元素小。因此,可以再输入序列中将候选子序列链接起来。)

LONG-MONOTONIC(A)let B[1..n] be a new array where every value = ∞let C[1..n] be a new arrayL = 1for i = 1 to nif A[i] < B[1]B[1] = A[i]C[1].head.key = A[i]elselet j be the largest index of B such that B[j] < A[i]B[j + 1] = A[i]C[j + 1] = C[j]INSERT(C[j + 1], A[i])if j + 1 > LL = L + 1print C[L]

最优二叉搜索树

最优二叉搜索树 (optimal binary search tree)问题的形式化定义如下:给定一个由 n 个互异的关键字组成的序列 K = < k1 , k2 , … , kn >,且关键字有序(有 k1 < k2 < … < kn ),我们要从这些关键字中构造一棵二叉查找树。对每个关键字 ki ,一次搜索为 ki 的概率是 pi 。某些搜索的值可能不在 K 内,因此还有 n + 1 个“虚拟键” d0 , d1 , … , dn 代表不在 K 内的值。其中, d0 代表所有小于 k1 的值, dn 代表所有大于 kn 的值,而对于 i = 1, 2, …, n - 1 ,虚拟键 di 代表所有位于 ki 和 ki+1 之间的值。对每个虚拟键 di ,一次搜索对应于 di 的概率是 qi 。下图是 n = 5个关键字的集合上的两棵二叉查找树。

其中 depthT 代表树 T 内一个结点的深度。
对给定的一组概率,我们的目标是构造一个期望搜索代价最小的二叉查找树。把这种树称为最优二叉查找树。下面将使用动态规划方法来解决这个问题。

步骤1:一棵最优二叉查找树的结构

为描述一棵最优二叉查找树的最优子结构,首先要看它的子树。一棵二叉查找树的任意一棵子树必定包含在连续范围内的关键字 ki ,…, kj ,有 1 <= i <= j <= n 。另外,一棵含有关键字 ki ,…, kj 的子树必定也含有虚拟键 di-1 ,…, dj 作为叶子。

现在我们可以描述最优子结构:如果一棵最优二叉查找树 T 有一棵包含关键字 ki ,…, kj 的子树 T’ ,那么这棵子树 T‘ 对于关键字 ki ,…, kj 和虚拟键 di-1 ,…, dj 的子问题也必定是最优的。

当ak为根结点树为最优,如果左子树不是最优的,那树不是最优,和前提不符,即子树必须是最优

使用最优子结构来说明可以根据子问题的最优解,来构造原问题的一个最优解。给定关键字 ki ,…, kj ,假设 kr ( i <= r <= j ),将是包含这些键的一棵最优子树的根。根 kr 的左子树包含关键字 ki ,…, kr-1 (和虚拟键 di-1 ,…, dr-1 ),右子树包含关键字 kr+1 ,…, kj (和虚拟键 dr ,…, dj )。我们只要检查所有的候选根 kr ,并且确定所有包含关键字 ki ,…, kr-1 和 kr+1 ,…, kj 的最优二叉查找树,就可以保证找到一棵最优的二叉查找树。

步骤2:一个递归解


假设我们知道该采用哪一个结点 kr 作为根。我们选择有最低期望搜索代价的结点作为根,从而得到最终的递归式:

步骤3:计算一棵最优二叉查找树的期望搜索代价

下面的伪码以概率 p1 ,…, pn 和 q1 ,…, qn 以及规模为 n 为输入,返回表 e 和 root 。

OPTIMAL-BST(p, q, n)
1  let e[1 .. n + 1, 0 .. n], w[1 .. n + 1, 0 .. n] and root[1 .. n, 1 .. n] be new tables
2  for i = 1 to n + 1
3      e[i, i - 1] = q_i - 1
4      w[i, i - 1] = q_i - 1
5  for l = 1 to n
6      for i = 1 to n - l + 1
7          j = i + l - 1
8          e[i, j] = ∞
9          w[i, j] = w[i, j - 1] + p_j + q_j
10         for r = i to j
11             t = e[i, r - 1] + e[r + 1, j] + w[i, j]
12             if t < e[i, j]
13                 e[i, j] = t
14                 root[i, j] = r
15 return e and root

下图是根据上面二叉查找树的关键字分布,程序 OPTIMAL-BST 计算出的表 e [ i , j ]和 w [ i , j ]和 root [ i , j ]。

练习

5-1 设计代码CONSTRUCT-OPTIMAL-BST(root),输入为表root,输出是最优二叉搜索树的结构。

CONSTRUCT-OPTIMAL-BST(root, i, j, last)if i == jreturnif last == 0print root[i, j] + "is the root"else if j < lastprint root[i, j] + "is the left child of" + lastelseprint root[i, j] + "is the right child of" + lastCONSTRUCT-OPTIMAL-BST(root, i, root[i, j] - 1, root[i, j])CONSTRUCT-OPTIMAL-BST(root, root[i, j] + 1, j, root[i, j])

5-2

5-[3-4]

(略)

思考题

todo

主要参考

《算法导论读书笔记(17)》
《算法导论 — 15.2 矩阵链乘法》
《算法导论 — 15.3 动态规划原理》
《算法导论读书笔记(18)》
《最优二叉查找树(动态规划)》
《算法导论读书笔记(19)》
《算法导论第十五章动态规划》
《Dynamic Programming》

《算法导论3rd第十五章》动态规划相关推荐

  1. 《算法导论3rd第二十五章》所有结点对的最短路径问题

    前言 本章节,我们考虑"如何找到一个图中所有结点之间的最短路径"问题.我们通过运行|V|次上章节的单源最短路径算法解决所有结点对之间的最短路径问题,只需要每一次使用一个不同的结点作 ...

  2. 《算法导论3rd第十九章》斐波那契堆

    前言 第六章堆排序使用了普通的二叉堆性质.其基本操作性能相当好,但union性能相当差. 对于一些图算法问题,EXTRACT-MIN 和DELETE操作次数远远小于DECREASE-KEY.因此有了斐 ...

  3. 《算法导论3rd第十六章》贪心算法

    前言 适用于最优化问题的算法往往包含一系列步骤,每个步骤都面临多种选择.使用动态规划解决最优化问题,相当于计算出每咱选择,浪费大量效率.对于"特定"下的最优化问题,可以使用更简单更 ...

  4. 第十五章 动态规划(最优二叉搜索树)

    第15章动态规划(最优二叉搜索树) 15.5 最优二叉搜索树 15.5 练习 15.5-1 15.5-2 15.5-3 15.5-4 说在前面的话: 为什么单独拿出来发? 1.由于排版篇幅问题,放一起 ...

  5. 《算法导论》第十二章——二叉搜索树

      虽然写这个博客主要目的是为了给我自己做一个思路记忆录,但是如果你恰好点了进来,那么先对你说一声欢迎.我并不是什么大触,只是一个菜菜的学生,如果您发现了什么错误或者您对于某些地方有更好的意见,非常欢 ...

  6. 第十五章 动态规划——最优二叉搜索树

    1.前言: 接着学习动态规划方法,最优二叉查找树问题.二叉查找树参考http://www.cnblogs.com/Anker/archive/2013/01/28/2880581.html.如果在二叉 ...

  7. 第十五章 - 垃圾回收相关算法

    第十五章 - 垃圾回收相关算法 文章目录 第十五章 - 垃圾回收相关算法 1.标记阶段:引用计数算法 1.1 垃圾标记阶段:对象存活判断 1.2 引用计数算法 1.3 小结 2.标记阶段:可达性分析算 ...

  8. 程序员编程艺术第三十四~三十五章:格子取数问题,完美洗牌算法

    第三十四~三十五章:格子取数,完美洗牌算法 作者:July.caopengcs.绿色夹克衫.致谢:西芹_new,陈利人, Peiyush Jain,白石,zinking. 时间:二零一三年八月二十三日 ...

  9. 程序员编程艺术第三十四 三十五章 格子取数问题,完美洗牌算法

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 第三十四 ...

最新文章

  1. 网络模型和TCP协议族
  2. 关于Ehcache缓存中timeToLiveSeconds和timeToIdleSeconds
  3. 代码习惯---打印参数
  4. Facebook 会沦落为二十年前的微软吗?
  5. 刘强东成京东数科二股东;高通再诉苹果;金立否认裁定破产 | 极客头条
  6. python具有可嵌入性_如何构建可嵌入Python
  7. 【转载】"library not found for - "解决办法
  8. 软考程序员2017下半年真题含答案解析
  9. DELPHI利WMI获得硬盘参数
  10. C#窗体标准计算器(下) 初级新手请多担待。
  11. QQ的clientkey与淘宝旺旺Token 不同平台环境下的登录认证
  12. 第三方网站接入秀米,实现秀米编辑的文章同步到自己的网站平台
  13. 关于USB鼠标驱动部分及问题解决
  14. mme 服务器位置,NB-IOT的网络架构简介
  15. 将微信小程序转换uniapp进行迁移的步骤以及遇到的问题总结
  16. 数字电子钟仿真软件中的电路测试,如何利用Multisim仿真软件进行数字电子钟设计...
  17. 线程传值数据丢失_开放线程:如何防止数据丢失
  18. 树的遍历(先序、中序、后序详解)
  19. 微商靠什么引流?微商有哪些平台可以精准引流?
  20. JavaScript幸运数字游戏

热门文章

  1. iOS App 稳定性指标及监测(转载)
  2. 教资_综合素质(必背内容)
  3. 【Matlab】求解积分问题
  4. EAP和EAPOL资料
  5. 最新《阿里巴巴Java开发手册》华山版、泰山版、嵩山版速来领取!
  6. 【Python奇淫技巧】用pandas的read_html函数仅一行代码实现网页爬虫
  7. Ubuntu18.04 安装 gcc
  8. 状态空间系统不完全可控的极点配置
  9. 独孤思维:买项目上当了?
  10. Android移动支付(支付宝支付2017最新接入详解)