###笔记
二叉搜索树满足如下性质:假设
x
x
x是二叉搜索树中的一个结点。如果
l
l
l是
x
x
x的左子树的一个结点,那么
l
.
k
e
y
≤
x
.
k
e
y
l.key ≤ x.key
l.key≤x.key。如果
r
r
r是
x
x
x的右子树的一个结点,那么
r
.
k
e
y
≥
x
.
k
e
y
r.key ≥ x.key
r.key≥x.key。
也就是说,二叉搜索树中的任意一个结点,它的左子树中的所有结点都不大于它,它的右子树中的所有结点都不小于它。下图给出了一个二叉搜索树的例子。
最优二叉搜索树(Optimal Binary Search Tree)问题描述如下。给定一个
n
n
n个不同关键字的已排序的序列
K
[
1..
n
]
=
<
k
1
,
k
2
,
…
,
k
n
>
K[1..n]= <k_1, k_2, …, k_n>
K[1..n]=<k1,k2,…,kn>(因此
k
1
<
k
2
<
…
<
k
n
k_1 < k_2 < … < k_n
k1<k2<…<kn),我们希望用这些关键字构造一个二叉搜索树。对每个关键字
k
i
k_i
ki,都有一个概率
p
i
p_i
pi表示其搜索概率。搜索过程中有可能遇到不在
K
[
1..
n
]
K[1..n]
K[1..n]中的元素,因此我们还有
n
+
1
n+1
n+1个元素的“伪关键字”序列
D
[
0..
n
]
=
<
d
0
,
d
1
,
d
2
,
…
,
d
n
>
D[0..n] = <d_0, d_1, d_2, …, d_n>
D[0..n]=<d0,d1,d2,…,dn>,表示搜索过程中可能遇到的所有不在
K
[
1..
n
]
K[1..n]
K[1..n]中的元素。
d
0
d_0
d0表示所有小于
k
1
k_1
k1的元素;
d
n
d_n
dn表示所有大于
k
n
k_n
kn的元素;对
i
=
1
,
2
,
…
,
n
−
1
i = 1, 2, …, n-1
i=1,2,…,n−1,
d
i
d_i
di表示所有在
k
i
k_i
ki到
k
i
+
1
k_{i+1}
ki+1之间的元素。对每个伪关键字
d
i
d_i
di,也有一个概率
q
i
q_i
qi表示对应的搜索概率。在二叉搜索树中,伪关键字
d
i
d_i
di必然出现在叶结点上,关键字
k
i
k_i
ki必然出现在非叶结点上。每次搜索要么成功(找到某个关键字
k
i
k_i
ki),要么失败(找到某个伪关键字
d
i
d_i
di)。关键字和伪关键字的概率满足:
假定一次搜索的代价等于访问的结点数,也就是此次搜索找到的结点在二叉搜索树中的深度再加
1
1
1。给定一棵二叉搜索树
T
T
T,我们可以确定进行一次搜索的期望代价。
其中
d
e
p
t
h
T
depth_T
depthT表示一个结点在二叉搜索树
T
T
T中的深度。
对于一组给定的关键字和伪关键字,以及它们对应的概率,我们希望构造一棵期望搜索代价最小的二叉搜索树,这称之为最优二叉搜索树。现在我们用动态规划方法来求解最优二叉搜索树问题。
首先我们描述最优二叉搜索树问题的最优子结构:假设由关键字子序列
K
[
i
.
.
j
]
=
<
k
i
,
…
,
k
j
>
K[i..j] = <k_i, …, k_j>
K[i..j]=<ki,…,kj>和伪关键字子序列
D
[
i
−
1..
j
]
=
<
d
i
−
1
,
…
,
d
j
>
D[i-1..j] = <d_{i-1}, …, d_j>
D[i−1..j]=<di−1,…,dj>构成的一棵最优二叉搜索树以
k
r
(
i
≤
r
≤
j
)
k_r(i ≤ r ≤ j)
kr(i≤r≤j)为根结点。那么它的左子树由子序列
K
[
i
.
.
r
−
1
]
K[i..r-1]
K[i..r−1]和
D
[
i
−
1..
r
−
1
]
D[i-1..r-1]
D[i−1..r−1]构成,这颗左子树显然也是一棵最优二叉搜索树。同样,它的右子树由子序列
K
[
r
+
1..
j
]
K[r+1..j]
K[r+1..j]和
D
[
r
.
.
j
]
D[r..j]
D[r..j]构成,这颗右子树显然也是一棵最优二叉搜索树。
这里有一个值得注意的细节—空子树。如果包含子序列
K
[
i
.
.
j
]
K[i..j]
K[i..j]的最优二叉搜索树以
k
i
k_i
ki为根结点。根据最优子结构性质,它的左子树包含子序列
K
[
i
.
.
i
−
1
]
K[i..i-1]
K[i..i−1],这个子序列不包含任何关键字。但请注意,左子树仍然包含一个伪关键字
d
i
−
1
d_{i-1}
di−1。同理,如果选择
k
j
k_j
kj为根结点,那么右子树也不包含任何关键字,而只包含一个伪关键字
d
j
d_j
dj。
用
e
[
i
,
j
]
e[i, j]
e[i,j]表示包含关键字子序列
K
[
i
.
.
j
]
=
<
k
i
,
…
,
k
j
>
K[i..j] = <k_i, …, k_j>
K[i..j]=<ki,…,kj>的最优二叉搜索树的期望搜索代价。我们最终希望计算出
e
[
1
,
n
]
e[1, n]
e[1,n]。
对于
j
=
i
−
1
j = i-1
j=i−1的情况,由于子树只包含伪关键字
d
i
−
1
d_{i-1}
di−1,所以期望搜索代价为
e
[
i
,
i
−
1
]
=
q
i
−
1
e[i, i-1] = q_{i-1}
e[i,i−1]=qi−1。
当
j
≥
i
j ≥ i
j≥i时,我们要遍历以
k
i
,
k
i
+
1
,
…
,
k
j
k_i, k_{i+1}, …, k_j
ki,ki+1,…,kj作为根结点的情况,然后从中选择期望搜索代价最小的情况作为子问题的最优解。假设选择
k
r
(
i
≤
r
≤
j
)
k_r(i ≤ r ≤ j)
kr(i≤r≤j)作为根结点,那么子序列
K
[
i
.
.
r
−
1
]
K[i..r-1]
K[i..r−1]构成的最优二叉搜索树作为左子树,左子树的期望搜索代价为
e
[
i
,
r
−
1
]
e[i, r-1]
e[i,r−1];子序列
K
[
r
+
1..
j
]
K[r+1..j]
K[r+1..j]构成的最优二叉搜索树作为右子树,右子树的期望搜索代价为
e
[
r
+
1
,
j
]
e[r+1, j]
e[r+1,j]。
当一棵子树链接到一个根结点上时,子树中所有结点的深度都增加了
1
1
1,那么这棵子树的期望搜索代价的增加值为它的所有结点的概率之和。对于一棵包含子序列
K
[
i
.
.
j
]
K[i..j]
K[i..j]的子树,所有结点的概率之和为
接上文,若
k
r
(
i
≤
r
≤
j
)
k_r(i ≤ r ≤ j)
kr(i≤r≤j)作为包含关键字子序列
K
[
i
.
.
j
]
K[i..j]
K[i..j]的最优二叉搜索树的根结点,可以得到如下公式
e
[
i
,
j
]
=
p
r
+
(
e
[
i
,
r
−
1
]
+
w
[
i
,
r
−
1
]
)
+
(
e
[
r
+
1
,
j
]
+
w
[
r
+
1
,
j
]
)
e[i,j]=p_r+(e[i,r-1]+w[i,r-1])+(e[r+1,j]+w[r+1,j])
e[i,j]=pr+(e[i,r−1]+w[i,r−1])+(e[r+1,j]+w[r+1,j])
由于
w
[
i
,
j
]
=
w
[
i
,
r
−
1
]
+
p
r
+
w
[
r
+
1
,
j
]
w[i, j] = w[i, r-1] + p_r + w[r+1, j]
w[i,j]=w[i,r−1]+pr+w[r+1,j],所以上式可重写为
e
[
i
,
j
]
=
e
[
i
,
r
−
1
]
+
e
[
r
+
1
,
j
]
+
w
[
i
,
j
]
e[i,j]=e[i,r-1]+e[r+1,j]+w[i,j]
e[i,j]=e[i,r−1]+e[r+1,j]+w[i,j]
我们要遍历以
k
i
,
k
i
+
1
,
…
,
k
j
k_i, k_{i+1}, …, k_j
ki,ki+1,…,kj作为根结点的情况,并选择期望搜索代价最小的情况作为子问题的最优解。于是我们可以得到下面的递归式。
e
[
i
,
j
]
e[i, j]
e[i,j]给出了最优二叉搜索树子问题的期望搜索代价。我们还需要记录最优二叉搜索树子问题的根结点,用
r
o
o
t
[
i
,
j
]
root[i, j]
root[i,j]来记录。
根据上文给出的递归式,我们可以采用自下而上的动态规划方法来求解最优二叉搜索树问题。下面给出了伪代码。
我们可以看到,该问题的求解过程与15.2节矩阵链乘法问题是很相似的。该算法的时间复杂度也与矩阵链乘法问题一样,都为
Θ
(
n
3
)
Θ(n^3)
Θ(n3)。
###习题
15.5-1 设计伪代码
CONSTRUCT-OPTIMAL-BST(root)
\text{CONSTRUCT-OPTIMAL-BST(root)}
CONSTRUCT-OPTIMAL-BST(root),输入为表
r
o
o
t
root
root,输出是最优二叉搜索树的结构。例如,对图15-10中的
r
o
o
t
root
root表,应输出
k
2
k_2
k2为根
k
1
k_1
k1为
k
2
k_2
k2的左孩子
d
0
d_0
d0为
k
1
k_1
k1的左孩子
d
1
d_1
d1为
k
1
k_1
k1的右孩子
k
5
k_5
k5为
k
2
k_2
k2的右孩子
k
4
k_4
k4为
k
5
k_5
k5的左孩子
k
3
k_3
k3为
k
4
k_4
k4的左孩子
d
2
d_2
d2为
k
3
k_3
k3的左孩子
d
3
d_3
d3为
k
3
k_3
k3的右孩子
d
4
d_4
d4为
k
4
k_4
k4的右孩子
d
5
d_5
d5为
k
5
k_5
k5的右孩子
与图15-9(b)中的最优二叉搜索树对应。
解
15.5-2 若
7
7
7个关键字的概率如下所示,求其最优二叉搜索树的结构和代价。
i i i | 0 0 0 | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 | 6 6 6 | 7 7 7 |
---|---|---|---|---|---|---|---|---|
p i p_i pi | 0.04 0.04 0.04 | 0.06 0.06 0.06 | 0.08 0.08 0.08 | 0.02 0.02 0.02 | 0.10 0.10 0.10 | 0.12 0.12 0.12 | 0.14 0.14 0.14 | |
q i q_i qi | 0.06 0.06 0.06 | 0.06 0.06 0.06 | 0.06 0.06 0.06 | 0.06 0.06 0.06 | 0.05 0.05 0.05 | 0.05 0.05 0.05 | 0.05 0.05 0.05 | 0.05 0.05 0.05 |
解
最优二叉搜索树如下所示。期望搜索代价为3.12。
15.5-3 假设
OPTIMAL-BST
\text{OPTIMAL-BST}
OPTIMAL-BST不维护表
w
[
i
,
j
]
w[i, j]
w[i,j],而是在第9行利用公式(15.12)直接计算
w
[
i
,
j
]
w[i, j]
w[i,j],然后在第11行使用此值。如此改动会对渐近时间复杂性有何影响?
解
在每次迭代中,直接利用公式(15.12)计算
w
[
i
,
j
]
w[i, j]
w[i,j]需要的时间为
O
(
n
)
O(n)
O(n)。然而,
w
[
i
,
j
]
w[i, j]
w[i,j]的计算并不是在最后一层迭代中,并且计算
w
[
i
,
j
]
w[i, j]
w[i,j]的渐近时间与最后一层迭代的渐近时间是相同的。所以此改动并不改变
OPTIMAL-BST
\text{OPTIMAL-BST}
OPTIMAL-BST的渐近时间。
15.5-4 Knuth[212]已经证明,对所有
1
≤
i
<
j
≤
n
1 ≤ i < j ≤ n
1≤i<j≤n,存在最优二叉搜索树,其根满足
r
o
o
t
[
i
,
j
−
1
]
≤
r
o
o
t
[
i
,
j
]
≤
r
o
o
t
[
i
+
1
,
j
]
root[i, j-1] ≤ root[i, j] ≤ root[i+1, j]
root[i,j−1]≤root[i,j]≤root[i+1,j]。利用这一特性修改算法
OPTIMAL-BST
\text{OPTIMAL-BST}
OPTIMAL-BST,使得运行时间减少为
Θ
(
n
2
)
Θ(n^2)
Θ(n2)。
解
在代码一中,在处理每个子问题
[
i
,
j
]
[i, j]
[i,j]时,从
i
i
i到
j
j
j进行迭代(代码一第9行)。根据题目所给的结论,在处理每个子问题
[
i
,
j
]
[i, j]
[i,j]时,可以改为从
r
o
o
t
[
i
,
j
−
1
]
root[i, j-1]
root[i,j−1]到
r
o
o
t
[
i
+
1
,
j
]
root[i+1, j]
root[i+1,j]进行迭代。下面给出了伪代码。
现在分析
OPTIMAL-BST-KNUTH
\text{OPTIMAL-BST-KNUTH}
OPTIMAL-BST-KNUTH的运行时间。
先考虑子问题规模
l
>
1
l > 1
l>1的情况。规模为
l
l
l的子问题一共有
n
−
l
+
1
n-l+1
n−l+1个(参见代码三第6行)。每个规模为
l
l
l的子问题包含
(
r
o
o
t
[
i
+
1
,
j
]
−
r
o
o
t
[
i
,
j
−
1
]
+
1
)
(root[i+1, j] - root[i, j-1] + 1)
(root[i+1,j]−root[i,j−1]+1)次迭代(其中
j
=
i
+
l
−
1
j = i+l-1
j=i+l−1),故求解所有规模为
l
l
l的子问题所需的迭代次数为
故求解所有规模为
l
(
l
>
1
)
l (l > 1)
l(l>1)的问题的迭代次数为
Θ
(
n
)
Θ(n)
Θ(n)。而每次迭代时间为
Θ
(
1
)
Θ(1)
Θ(1),故求解所有规模为
l
l
l的子问题的时间为
Θ
(
n
)
Θ(n)
Θ(n)。
而规模为
l
=
1
l = 1
l=1的子问题一共有
n
n
n个,每个子问题需要
Θ
(
1
)
Θ(1)
Θ(1)。故求解所有规模为
l
=
1
l = 1
l=1的子问题需要
Θ
(
n
)
Θ(n)
Θ(n)时间。
子问题的规模
l
l
l的取值有
n
n
n种情况,故
OPTIMAL-BST-KNUTH
\text{OPTIMAL-BST-KNUTH}
OPTIMAL-BST-KNUTH的运行时间为
Θ
(
n
2
)
Θ(n^2)
Θ(n2)。
本节相关的code链接。
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter15/Section_15.5