前言
和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的。分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题。在这种情况下,分治法会重复地求解公共的子子问题。而动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重复。
动态规划通常用于最优化问题 。此类问题可能有多种可行解。每个解有一个值,而我们希望找出具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因而可能存在多个最优解。
动态规划算法的设计可以分为如下4个步骤:
- 描述最优解的结构
- 递归定义最优解的值
- 按自底向上的方式计算最优解的值
- 由计算出的结构构造一个最优解
钢条切割
塞林企业会买进长钢条,将它们切割成短条后卖出(切割是免费的,不计成本)。塞林企业的老总想要知道钢条怎么切割最赚钱。
已知塞林企业对长度为 i 英寸的钢条的售价为 pi 美元,其中 i = 1,2,…。下图给出了一张样本价格表。
一根长度为n的钢条,每个切口都有切和不切的选择,即 共有
2
n
−
1
2^{n-1}
2n−1 种不同的切割方式。我们可以将最优收益
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+rn−1,r2+rn−2,…,rn−1+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}
rn−i 之后做和。再把
p
n
=
r
0
+
r
n
−
0
p_n = r_0 + r_{n-0}
pn=r0+rn−0得出一种更简单的递归结构
r
n
=
m
a
x
(
r
i
+
r
n
−
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 )的规模可以帮助我们确定动态规划的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度成正比,而子问题的数目等于子问题的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系。
重构解
上面的算法仅返回最优解的收益值 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 pi−1∗pi,求完全的括号化方案,使得计算乘积 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。
- 如果 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 ⟩ 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,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] == 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》