《算法导论3rd第十五章》动态规划
前言
和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的。分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题。在这种情况下,分治法会重复地求解公共的子子问题。而动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重复。
动态规划通常用于最优化问题 。此类问题可能有多种可行解。每个解有一个值,而我们希望找出具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因而可能存在多个最优解。
动态规划算法的设计可以分为如下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_nA1A2...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。
- 如果 xm = yn ,则 zk = xm = yn 且 Zk-1 是 Xm-1 和 Yn-1 的一个LCS。
- 如果 xm ≠ yn ,那么 zk ≠ xm 意味着 Z 是 Xm-1 和 Y 的一个LCS。
- 如果 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第十五章》动态规划相关推荐
- 《算法导论3rd第二十五章》所有结点对的最短路径问题
前言 本章节,我们考虑"如何找到一个图中所有结点之间的最短路径"问题.我们通过运行|V|次上章节的单源最短路径算法解决所有结点对之间的最短路径问题,只需要每一次使用一个不同的结点作 ...
- 《算法导论3rd第十九章》斐波那契堆
前言 第六章堆排序使用了普通的二叉堆性质.其基本操作性能相当好,但union性能相当差. 对于一些图算法问题,EXTRACT-MIN 和DELETE操作次数远远小于DECREASE-KEY.因此有了斐 ...
- 《算法导论3rd第十六章》贪心算法
前言 适用于最优化问题的算法往往包含一系列步骤,每个步骤都面临多种选择.使用动态规划解决最优化问题,相当于计算出每咱选择,浪费大量效率.对于"特定"下的最优化问题,可以使用更简单更 ...
- 第十五章 动态规划(最优二叉搜索树)
第15章动态规划(最优二叉搜索树) 15.5 最优二叉搜索树 15.5 练习 15.5-1 15.5-2 15.5-3 15.5-4 说在前面的话: 为什么单独拿出来发? 1.由于排版篇幅问题,放一起 ...
- 《算法导论》第十二章——二叉搜索树
虽然写这个博客主要目的是为了给我自己做一个思路记忆录,但是如果你恰好点了进来,那么先对你说一声欢迎.我并不是什么大触,只是一个菜菜的学生,如果您发现了什么错误或者您对于某些地方有更好的意见,非常欢 ...
- 第十五章 动态规划——最优二叉搜索树
1.前言: 接着学习动态规划方法,最优二叉查找树问题.二叉查找树参考http://www.cnblogs.com/Anker/archive/2013/01/28/2880581.html.如果在二叉 ...
- 第十五章 - 垃圾回收相关算法
第十五章 - 垃圾回收相关算法 文章目录 第十五章 - 垃圾回收相关算法 1.标记阶段:引用计数算法 1.1 垃圾标记阶段:对象存活判断 1.2 引用计数算法 1.3 小结 2.标记阶段:可达性分析算 ...
- 程序员编程艺术第三十四~三十五章:格子取数问题,完美洗牌算法
第三十四~三十五章:格子取数,完美洗牌算法 作者:July.caopengcs.绿色夹克衫.致谢:西芹_new,陈利人, Peiyush Jain,白石,zinking. 时间:二零一三年八月二十三日 ...
- 程序员编程艺术第三十四 三十五章 格子取数问题,完美洗牌算法
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 第三十四 ...
最新文章
- 网络模型和TCP协议族
- 关于Ehcache缓存中timeToLiveSeconds和timeToIdleSeconds
- 代码习惯---打印参数
- Facebook 会沦落为二十年前的微软吗?
- 刘强东成京东数科二股东;高通再诉苹果;金立否认裁定破产 | 极客头条
- python具有可嵌入性_如何构建可嵌入Python
- 【转载】"library not found for - "解决办法
- 软考程序员2017下半年真题含答案解析
- DELPHI利WMI获得硬盘参数
- C#窗体标准计算器(下) 初级新手请多担待。
- QQ的clientkey与淘宝旺旺Token 不同平台环境下的登录认证
- 第三方网站接入秀米,实现秀米编辑的文章同步到自己的网站平台
- 关于USB鼠标驱动部分及问题解决
- mme 服务器位置,NB-IOT的网络架构简介
- 将微信小程序转换uniapp进行迁移的步骤以及遇到的问题总结
- 数字电子钟仿真软件中的电路测试,如何利用Multisim仿真软件进行数字电子钟设计...
- 线程传值数据丢失_开放线程:如何防止数据丢失
- 树的遍历(先序、中序、后序详解)
- 微商靠什么引流?微商有哪些平台可以精准引流?
- JavaScript幸运数字游戏