###笔记
二叉搜索树满足如下性质:假设
是二叉搜索树中的一个结点。如果
是
的左子树的一个结点,那么
。如果
是
的右子树的一个结点,那么
。
也就是说,二叉搜索树中的任意一个结点,它的左子树中的所有结点都不大于它,它的右子树中的所有结点都不小于它。下图给出了一个二叉搜索树的例子。
最优二叉搜索树(Optimal Binary Search Tree)问题描述如下。给定一个
个不同关键字的已排序的序列
(因此
),我们希望用这些关键字构造一个二叉搜索树。对每个关键字
,都有一个概率
表示其搜索概率。搜索过程中有可能遇到不在
中的元素,因此我们还有
个元素的“伪关键字”序列
,表示搜索过程中可能遇到的所有不在
中的元素。
表示所有小于
的元素;
表示所有大于
的元素;对
,
表示所有在
到
之间的元素。对每个伪关键字
,也有一个概率
表示对应的搜索概率。在二叉搜索树中,伪关键字
必然出现在叶结点上,关键字
必然出现在非叶结点上。每次搜索要么成功(找到某个关键字
),要么失败(找到某个伪关键字
)。关键字和伪关键字的概率满足:
假定一次搜索的代价等于访问的结点数,也就是此次搜索找到的结点在二叉搜索树中的深度再加
。给定一棵二叉搜索树
,我们可以确定进行一次搜索的期望代价。
其中
表示一个结点在二叉搜索树
中的深度。
对于一组给定的关键字和伪关键字,以及它们对应的概率,我们希望构造一棵期望搜索代价最小的二叉搜索树,这称之为最优二叉搜索树。现在我们用动态规划方法来求解最优二叉搜索树问题。
首先我们描述最优二叉搜索树问题的最优子结构:假设由关键字子序列
和伪关键字子序列
构成的一棵最优二叉搜索树以
为根结点。那么它的左子树由子序列
和
构成,这颗左子树显然也是一棵最优二叉搜索树。同样,它的右子树由子序列
和
构成,这颗右子树显然也是一棵最优二叉搜索树。
这里有一个值得注意的细节—空子树。如果包含子序列
的最优二叉搜索树以
为根结点。根据最优子结构性质,它的左子树包含子序列
,这个子序列不包含任何关键字。但请注意,左子树仍然包含一个伪关键字
。同理,如果选择
为根结点,那么右子树也不包含任何关键字,而只包含一个伪关键字
。
用
表示包含关键字子序列
的最优二叉搜索树的期望搜索代价。我们最终希望计算出
。
对于
的情况,由于子树只包含伪关键字
,所以期望搜索代价为
。
当
时,我们要遍历以
作为根结点的情况,然后从中选择期望搜索代价最小的情况作为子问题的最优解。假设选择
作为根结点,那么子序列
构成的最优二叉搜索树作为左子树,左子树的期望搜索代价为
;子序列
构成的最优二叉搜索树作为右子树,右子树的期望搜索代价为
。
当一棵子树链接到一个根结点上时,子树中所有结点的深度都增加了
,那么这棵子树的期望搜索代价的增加值为它的所有结点的概率之和。对于一棵包含子序列
的子树,所有结点的概率之和为
接上文,若
作为包含关键字子序列
的最优二叉搜索树的根结点,可以得到如下公式
由于
,所以上式可重写为
我们要遍历以
作为根结点的情况,并选择期望搜索代价最小的情况作为子问题的最优解。于是我们可以得到下面的递归式。
给出了最优二叉搜索树子问题的期望搜索代价。我们还需要记录最优二叉搜索树子问题的根结点,用
来记录。
根据上文给出的递归式,我们可以采用自下而上的动态规划方法来求解最优二叉搜索树问题。下面给出了伪代码。
我们可以看到,该问题的求解过程与15.2节矩阵链乘法问题是很相似的。该算法的时间复杂度也与矩阵链乘法问题一样,都为
。
###习题
15.5-1 设计伪代码
,输入为表
,输出是最优二叉搜索树的结构。例如,对图15-10中的
表,应输出
为根
为
的左孩子
为
的左孩子
为
的右孩子
为
的右孩子
为
的左孩子
为
的左孩子
为
的左孩子
为
的右孩子
为
的右孩子
为
的右孩子
与图15-9(b)中的最优二叉搜索树对应。
解
15.5-2 若
个关键字的概率如下所示,求其最优二叉搜索树的结构和代价。
解
最优二叉搜索树如下所示。期望搜索代价为3.12。
15.5-3 假设
不维护表
,而是在第9行利用公式(15.12)直接计算
,然后在第11行使用此值。如此改动会对渐近时间复杂性有何影响?
解
在每次迭代中,直接利用公式(15.12)计算
需要的时间为
。然而,
的计算并不是在最后一层迭代中,并且计算
的渐近时间与最后一层迭代的渐近时间是相同的。所以此改动并不改变
的渐近时间。
15.5-4 Knuth[212]已经证明,对所有
,存在最优二叉搜索树,其根满足
。利用这一特性修改算法
,使得运行时间减少为
。
解
在代码一中,在处理每个子问题
时,从
到
进行迭代(代码一第9行)。根据题目所给的结论,在处理每个子问题
时,可以改为从
到
进行迭代。下面给出了伪代码。
现在分析
的运行时间。
先考虑子问题规模
的情况。规模为
的子问题一共有
个(参见代码三第6行)。每个规模为
的子问题包含
次迭代(其中
),故求解所有规模为
的子问题所需的迭代次数为
故求解所有规模为
的问题的迭代次数为
。而每次迭代时间为
,故求解所有规模为
的子问题的时间为
。
而规模为
的子问题一共有
个,每个子问题需要
。故求解所有规模为
的子问题需要
时间。
子问题的规模
的取值有
种情况,故
的运行时间为
。
本节相关的code链接。
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter15/Section_15.5