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

前言

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

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

钢条切割

塞林企业会买进长钢条,将它们切割成短条后卖出(切割是免费的,不计成本)。塞林企业的老总想要知道钢条怎么切割最赚钱。
已知塞林企业对长度为 i 英寸的钢条的售价为 pi 美元,其中 i = 1,2,…。下图给出了一张样本价格表。
在这里插入图片描述
一根长度为n的钢条,每个切口都有切和不切的选择,即 共有 2 n − 1 2^{n-1} 2n1 种不同的切割方式。我们可以将最优收益 r n r_n rn 表示成如下形式:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , … , r n − 1 + r 1 ) r_n = max ( p_n , r_1 + r_{n-1} , r_2 + r_{n-2} ,…, r_{n-1} + r_1 ) rn=max(pn,r1+rn1,r2+rn2,,rn1+r1)
第一个参数 p n p_n pn 代表不切割时钢条的价格。其它的 n - 1个参数首先将钢条分为2份,长度分别为 i 和 n - i ( i = 1,2,…, n - 1),然后分别取得两份的最优收益 r i r_i ri r n − i r_{n-i} rni 之后做和。再把 p n = r 0 + r n − 0 p_n = r_0 + r_{n-0} pn=r0+rn0得出一种更简单的递归结构
r n = m a x ( r i + r n − i ) r_n = max ( r_i + r_{n-i} ) rn=max(ri+rni)

自顶向下的递归实现
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 )的规模可以帮助我们确定动态规划的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度成正比,而子问题的数目等于子问题的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系

重构解

上面的算法仅返回最优解的收益值 r n r_n rn,并未返回如何切割

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 array
    r[0] = 0
    for j = 1 to n
        q = p[j]
        for i = 1 to j - 1
            q = max(q, p[i] + r[j - i] - c)
        r[j] = q
    return r[n]
1-4 修改MEMOIZED-CUT-ROD,使之不仅返回最优收益值,还返回切割方案。
MEMOIZED-CUT-ROD(p, n)
    let r[0..n] and s[0..n] be new arrays
    for i = 0 to n
        r[i] = -∞
    (val, s) = MEMOIZED-CUT-ROD-AUX(p, n, r, s)
    print "The optimal value is" val "and the cuts are at" s
    j = n
    while j > 0
        print s[j]
        j = j - s[j]
        
MEMOIZED-CUT-ROD-AUX(p, n, r, s)
    if r[n]0
        return r[n]
    if n == 0
        q = 0
    else q = -∞
        for i = 1 to n
            (val, s) = MEMOIZED-CUT-ROD-AUX(p, n - i, r, s)
            if q < p[i] + val
                q = p[i] + val
                s[n] = i
    r[n] = q
    return (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 array
    fib[0] = 1
    fib[1] = 1
    for i = 2 to n
        fib[i] = fib[i - 1] + fib[i - 2]
    return fib[n]

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

矩阵链乘法

给定n个矩阵构成的一个链 < A 1 , A 2 , A 3 , . . . . . . . A n > <A_1,A_2,A_3,.......A_n> <A1,A2,A3,.......An>,其中i=1,2,…n,矩阵A的维数为 p i − 1 ∗ p i p_{i-1}* p_i pi1pi,求完全的括号化方案,使得计算乘积 A 1 A 2 . . . A n A_1A_2...A_n A1A2...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 n
 4       do m[i][i] = 0;
 5   for t = 2 to n  //t is the chain length
 6        do for i=1 to n-t+1
 7                      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"
   else
      print "(";
      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 == j
        return A[i]
    if i + 1 == j
        return 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 = < x 1 , x 2 , … , x m > X = < x_1 , x_2 , … , x_m > X=<x1,x2,,xm>,另一个序列 Z = < z 1 , z 2 , … , z k > Z = < z_1 , z_2 , … , z_k > Z=<z1,z2,,zk>,如果 Z 满足如下条件则称 Z 为 X 的 子序列 (subsequence),即存在一个严格递增的 X 的下标序列 < i 1 , i 2 , … , i k > < i_1 , i_2 , … , i_k > <i1,i2,,ik>,对所有 j = 1,2,…, k ,满足 x i = z j x_i = z_j xi=zj 。给定两个序列 X 和 Y ,如果 Z 既是 X 的子序列,也是 Y 的子序列,则称它是 X 和 Y 的 公共子序列 。
最长公共子序列问题 (longest-common-subsequence problem)就是给定两个序列 X = < x 1 , x 2 , … , x m > X = < x_1 , x_2 , … , x_m > X=<x1,x2,,xm> Y = < y 1 , y 2 , … , y n > Y = < y_1 , y_2 , … , y_n > Y=<y1,y2,,yn>,求 X 和 Y 长度最长的公共子序列。简称LCS问题。下面将展示如何用动态规划方法高效求解LCS问题。

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

子问题的自然分类对应两个输入序列的“前缀”对。前缀的严格定义如下:给定一个序列 X = < x 1 , x 2 , … , x m > X = < x_1 , x_2 , … , x_m > X=<x1,x2,,xm>,对 i = 0,1,…, m ,定义 X 的第 i 前缀为 X i = < x 1 , x 2 , … , x i > , X 0 Xi = < x_1 , x_2 , … , x_i >, X_0 Xi=<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 ⟩ o r ⟨ 1 , 0 , 1 , 0 , 1 , 0 ⟩ . ⟨1,0,0,1,1,0⟩ or ⟨1,0,1,0,1,0⟩. 1,0,0,1,1,0or1,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] == 0
        return
    if 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)
    else
        PRINT-LCS(c, X, Y, i, j - 1)
4-3设计LCS-LENGTH的带备忘的版本,运行时间为O(mn);
MEMOIZED-LCS-LENGTH(X, Y, i, j)
    if c[i, j] > -1
        return c[i, j]
    if i == 0 or j == 0
        return c[i, j] = 0
    if x[i] == y[j]
        return c[i, j] = LCS-LENGTH(X, Y, i - 1, j - 1) + 1
    return 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 array
    i = X.length
    j = Y.length
    while i > 0 and j > 0
        if x[i] == y[j]
            s[n] = x[i]
            n = n - 1
            i = i - 1
            j = j - 1
        else if c[i - 1, j] ≥ c[i, j - 1]
            i = i - 1
        else j = j - 1
    for i = 1 to s.length
        print s[i]
        
MEMO-LCS-LENGTH-AUX(X, Y, c, b)
    m = |X|
    n = |Y|
    if c[m, n] != 0 or m == 0 or n == 0
        return
    if x[m] == y[n]
        b[m, n] = ↖
        c[m, n] = MEMO-LCS-LENGTH-AUX(X[1..m - 1], Y[1..n - 1], c, b) + 1
    else 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)
    else
        b[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 tables
    MEMO-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 array
    L = 1
    for i = 1 to n
        if A[i] < B[1]
            B[1] = A[i]
            C[1].head.key = A[i]
        else
            let 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 > L
                L = L + 1
    print 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 == j
        return
    if last == 0
        print root[i, j] + "is the root"
    else if j < last
        print root[i, j] + "is the left child of" + last
    else
        print root[i, j] + "is the right child of" + last
    CONSTRUCT-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

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值