算法导论 — 15.5 最优二叉搜索树

###笔记

二叉搜索树满足如下性质:假设 x x x是二叉搜索树中的一个结点。如果 l l l x x x的左子树的一个结点,那么 l . k e y ≤ x . k e y l.key ≤ x.key l.keyx.key。如果 r r r x x x的右子树的一个结点,那么 r . k e y ≥ x . k e y r.key ≥ x.key r.keyx.key
  
  也就是说,二叉搜索树中的任意一个结点,它的左子树中的所有结点都不大于它,它的右子树中的所有结点都不小于它。下图给出了一个二叉搜索树的例子。
  这里写图片描述
  最优二叉搜索树(Optimal Binary Search Tree)问题描述如下。给定一个 n n n个不同关键字的已排序的序列 K [ 1.. n ] = &lt; k 1 , k 2 , … , k n &gt; K[1..n]= &lt;k_1, k_2, …, k_n&gt; K[1..n]=<k1,k2,,kn>(因此 k 1 &lt; k 2 &lt; … &lt; k n k_1 &lt; k_2 &lt; … &lt; 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 ] = &lt; d 0 , d 1 , d 2 , … , d n &gt; D[0..n] = &lt;d_0, d_1, d_2, …, d_n&gt; 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,,n1 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 ] = &lt; k i , … , k j &gt; K[i..j] = &lt;k_i, …, k_j&gt; K[i..j]=<ki,,kj>和伪关键字子序列 D [ i − 1.. j ] = &lt; d i − 1 , … , d j &gt; D[i-1..j] = &lt;d_{i-1}, …, d_j&gt; D[i1..j]=<di1,,dj>构成的一棵最优二叉搜索树以 k r ( i ≤ r ≤ j ) k_r(i ≤ r ≤ j) kr(irj)为根结点。那么它的左子树由子序列 K [ i . . r − 1 ] K[i..r-1] K[i..r1] D [ i − 1.. r − 1 ] D[i-1..r-1] D[i1..r1]构成,这颗左子树显然也是一棵最优二叉搜索树。同样,它的右子树由子序列 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..i1],这个子序列不包含任何关键字。但请注意,左子树仍然包含一个伪关键字 d i − 1 d_{i-1} di1。同理,如果选择 k j k_j kj为根结点,那么右子树也不包含任何关键字,而只包含一个伪关键字 d j d_j dj
  
  用 e [ i , j ] e[i, j] e[i,j]表示包含关键字子序列 K [ i . . j ] = &lt; k i , … , k j &gt; K[i..j] = &lt;k_i, …, k_j&gt; K[i..j]=<ki,,kj>的最优二叉搜索树的期望搜索代价。我们最终希望计算出 e [ 1 , n ] e[1, n] e[1,n]
  
  对于 j = i − 1 j = i-1 j=i1的情况,由于子树只包含伪关键字 d i − 1 d_{i-1} di1,所以期望搜索代价为 e [ i , i − 1 ] = q i − 1 e[i, i-1] = q_{i-1} e[i,i1]=qi1
  
  当 j ≥ i j ≥ i ji时,我们要遍历以 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(irj)作为根结点,那么子序列 K [ i . . r − 1 ] K[i..r-1] K[i..r1]构成的最优二叉搜索树作为左子树,左子树的期望搜索代价为 e [ i , r − 1 ] e[i, r-1] e[i,r1];子序列 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(irj)作为包含关键字子序列 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,r1]+w[i,r1])+(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,r1]+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,r1]+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 &lt; j ≤ n 1 ≤ i &lt; j ≤ n 1i<jn,存在最优二叉搜索树,其根满足 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,j1]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,j1] 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 &gt; 1 l &gt; 1 l>1的情况。规模为 l l l的子问题一共有 n − l + 1 n-l+1 nl+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,j1]+1)次迭代(其中 j = i + l − 1 j = i+l-1 j=i+l1),故求解所有规模为 l l l的子问题所需的迭代次数为
        这里写图片描述
  故求解所有规模为 l ( l &gt; 1 ) l (l &gt; 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
  这里写图片描述

  • 53
    点赞
  • 196
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值