[CLRS][CH 15.5]最优二叉查找树

背景铺垫

假设我们正在设计一个翻译程序,讲英语翻译成法语,需要用一棵BST存储文章中出现的单词及等价的法语。因为要频繁地查找这棵树,所以我们希望查找时间越短越好。当然我们可以考虑使用红黑树,或者可能更适用的伸展树,来实现这一操作。但是这仍然不能满足我们的需要:由于单词出现频率不同,如果mycophagist这种奇怪的单词出现在根节点,而the、a、is这些常用单词出现在叶节点,即使O(lgn)的查找速度也极大的浪费了时间。

因此我们需要这样一种BST:频繁出现的单词出现在离根节点较近的地方。假设我们知道每个单词的出现频率,应该如何组织BST使得总搜索访问的节点数目最小呢?

最优二叉查找树 Optimal Binary Search Tree

定义:对于给定的一组概率,构造一棵搜索期望代价最小的BST,称为最优二叉查找树。

介绍:形式地,给定一个由n个互异关键字组成的序列 K = <k1, k2, ..., kn>,且关键字有序,即 k1 < k2 < ... <kn。利用这些关键字构造一棵BST,对于每个关键字ki搜索的概率是pi。某些搜索的值可能不在K内,因此还有n+1个“虚拟键” <d0, d1, d2, ..., dn> 代表不在K内的值。具体地,d0代表所有小于k1的值,dn代表所有大于kn的值。对于 i = 1, 2, ..., n-1,虚拟键di代表所有位于ki和ki+1之间的值。对于每个虚拟键di搜索的概率是qi。每个关键字ki是一个内部结点,而di是叶节点。每次搜索或者成功(找到内部结点ki),或者失败(找到叶节点di)。因此有:

  $\sum_{i=1}^{n}p_{i}+\sum_{i=0}^{n}q_{i}=1$

假设一次搜索的实际代价为检查的节点个数,亦即,在T内搜索所发现的节点的深度加上1。所以在T内一次搜索的期望代价为:

  $\begin{align*} E[T]&=\sum_{i=1}^{n}(depth_{T}(k_{i})+1)\cdot{} p_{i}+\sum_{i=0}^{n}(depth_{T}(d_{i})+1)\cdot{} q_{i}\\ &=\sum_{i=1}^{n}depth_{T}(k_{i})\cdot{} p_{i}+\sum_{i=0}^{n}depth_{T}(d_{i})\cdot{} q_{i}+1 \end{align*}$

举个栗子:

如图,根据相同关键字构造的BST,检索概率如下表

i 0 1 2 3 4 5
pi   0.15 0.10 0.05 0.10 0.20
qi 0.05 0.10 0.05 0.05 0.05 0.10

因此容易得出左树搜索代价为2.80,右树搜索代价为2.75,因此这棵树是最优的。

这个例子说明:一棵最优BST不一定是一棵整体高度最小的树,也不一定总是把有最大概率的关键字放在根部。

步骤1:一棵最优二叉树的结构

首先找最优子结构:如果一棵最优二叉查找树T有一棵子树T‘,包含关键字<ki, ..., kj>和<di-1, ..., dj>,那么树T‘的子树T‘‘也必定是一棵最优二叉查找树。这样,我们可以根据最优子结构来构造一棵最优二叉查找树。

给定关键字<ki, ..., kj>,假设根节点为k(i <= r <= j),则:
左子树包含关键字<ki, ..., kr-1>和<di-1, ..., dr-1>;
右子树包含关键字<kr+1, ..., kj>和<dr, ..., dj>。
只要我们检查所有候选根节点k(i <= r <= j),并且确定左右子树的最优子树,就保证能得到一棵最优二叉查找树。

注意一点是空子树的处理。如果一棵子树包含关键字<ki, ..., ki-1>,则其不含有任何关键字,但是却含有虚拟键di-1。同样的,对于子树<kj+1, ..., kj>,不含有任何关键字,但含有虚拟键dj

步骤2:一个递归解

选取子问题域为寻找包含关键字<ki, ..., kj>的最优二叉查找树,其中 i >= 1 且 i-1 <= j <= n(当 j = i-1 时只有虚拟键di-1)。定义e[i, j]为该问题的期望代价,显然,e[1, n]就是原问题的解。

当 j = i-1 时出现简单情况。因为只有一个虚拟键di-1,所以搜索期望代价是e[i, i-1] = qi-1。
当 j >= i 时需要遍历结点<ki, ..., ki-1>选择一个合适的根k(i <= r <= j),然后分别构造左右子树。同时,当一棵树成为另一棵树的子树时,子树所有结点深度加1。
对一颗有关键字<ki, ..., kj>的子树,定义概率的总和为:

  $w(i,j) = \sum_{j=i}^{j}pi+\sum_{j=i-1}^{j}ql$

因此,如果kr是一棵包含关键字<ki, ..., ki-1>的最优子树的根,则有

  $e[i,j]=p_{r}+((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)$

将e[i,j]重写为

  $e[i,j]=e[i,r-1]+e[r+1,j]+w(i,j)$

于是我们就能得出递归式。在这里假设我们已知结点kr,我们选择有最低期望搜索代价的结点作为根:

  $e[i,j]=\begin{cases} q_{i-1}&j=i-1\\\textrm{min}_{i\leq r\leq j}{e[i,r-1]+e[r+1,j]+w(i,j)}&i\leq j\end{cases}$

我们还定义root[i,j]为根kr的下标r。

步骤3:计算一棵最优二叉查找树的期望搜索代价

其实我们能发现最优二叉查找树和矩阵链乘法之间有些相似,比如子问题都是由连续下标子范围组成。

正如我们之前所做的,为了储存子问题的结果,我们需要把e[i,j]保存在表e[1..n+1, 0..n]中,其中:
第一维下标需要达到n+1而不是n,原因是为了有一个只包含虚拟键dn的子树,我们需要计算和保存e[n+1,n];
第二纬下标需要从0开始,是为了保存e[1,0]。

使用 j >= i-1 的表项 e[i,j] 和表 root[i,j] 来记录包含关键字<ki, ..., kj>的子树的根,这个表只用 1 <= i <= j <= n的表项。

为了提高效率,还需要一个表格。不是没当计算e[i,j]时都要从头开始计算w(i,j),而是把这些值保存在表w[1..n+1, 0..n]中。
计算基础情况 w[i,i-1] = qi-1,其中 1 <= i <= n;
计算 w[i,j] = w[i,j-1] + pj + qj,其中 i <= j;
因此,可以计算出O(n2)个w[i,j]的值,每一个值需要O(1)计算时间。

下面的代码中,以概率<p, ..., p>和<q, ..., q>以及结点数量n为参数,返回表e和root。运行时间为O(n3)。

OPTIMAL-BST(p, q, n)
for i = (1 to n+1)     // 1~3行: 循环初始化e[i, i-1]和w[i, i-1]的值
    e[i, i-1] = q[i-1] //
    w[i, i-1] = q[i-1] //
for l = (1 to n)            // 4~13行: 循环利用递归式计算e[i, j]和w[i, j]
    for i = (1 to n-l+1)    // 第一次迭代时, l=1, 计算e[i,i]和w[i,i], i = 1..n
        j = i+l-1           // 第二次迭代时, l=2, 计算e[i,i+1]和w[i,i+1], i = 1..n-1
        e[i, j] = ∞         // 如此往复
        w[i, j] = w[i, j-1] + p[j] + q[j]
        for r = (i to j)                        // 9~13行: 尝试每个下标r, 以确定根节点
            t = e[i, r-1] + e[r+1, j] + w[i, j] //
            if t < e[i, j]                      // 发现更好的下标值则在root[i,j]中保存当前r
                e[i, j] = t                     //
                root[i, j] = r                  //
return e and root

补充:优化OPTMAL-BST过程 (CLRS 15.5-4)

现已被证明,对于所有的 i <= i < j <= n,总存在最优子树的根使得 root[i, j-1] <= root[i, j] <= root[i+1, j]。
利用这个事实我们可以修改OPTMAL-BST过程,使其在O(n2)时间内执行。

将第九行改为:
for r = ( root[i, j-1] to root[i+1, j] )

证明:该算法运行时间为O(n2)。

这一结论可以通过证明每一个e[i, j]的运行时间都是O(n)而得出。我们定义 e[i, j] (其中 j - i = k) 的全部状态集合为第k层集合(显然集合中 n-k 个结点)。所以计算 e[i, j] 需要 root[i+1, j] - root[i, j-1] + 1 次循环。因此,对于所有第k层集合的元素而言,总共需要 root[k, 1] – root[1, k] + n – k 次循环。又因为 1 <= root[k, 1] 且 root[1, k] <= n,所以循环次数为O(n)。

因为 0 <= k <= n-1,所以总循环次数为O(n2)。

该解法由Rip‘s Infernal Majesty给出。

时间: 2024-10-10 23:59:46

[CLRS][CH 15.5]最优二叉查找树的相关文章

[CLRS][CH 15.4] 最长公共子序列

---恢复内容开始--- 摘要 介绍了最长公共子序列的概念及解题思路. 子序列概念 子序列:一个给定序列的子序列就是该给定序列中,去掉零个或多个元素.一般来说,给定一个序列 X = <x1, x2, ..., xm>,另一个序列 Z = <z1, z2, ..., zk> 如果存在X的一个严格递增下标序列<i1, i2, ..., ik>,使得所有的j = 1, 2, ..., k,有xij = zj,则Z是X的一个子序列.例如,Z = <B, C, D, B&g

[CLRS][CH 15.2] 动态规划之矩阵链乘法

摘要 整理了矩阵链乘法的动态规划思路. 题目 给定n个要相乘的矩阵构成的序列<A1, A2, ... , An>,其中 i=1, 2, ..., n,矩阵 Ai 的维数为pi-1*pi.计算乘积 A1A2...An 的最小代价的矩阵相乘循序. 补充:矩阵乘法满足结合律,例如,乘积 A1A2A3A4 共有五种不同加括号结合形式.不同的结合形式极大的影响运算效率.当且仅当矩阵A和B相容(A.列 = B.行)时,才可以计算矩阵乘法.例如:矩阵A为p*q, 矩阵B为q*r,则相乘后得到的矩阵C为p*r

[CLRS][CH 15.3] 动态规划基础

摘要 究竟什么时候才需要动态规划?这里介绍两个要素:最优子结构,重叠子问题.另外,还要分析一种方法——备忘录,充分利用重叠子问题性质. 最优子结构 利用动态规划求解时第一步是描述最优解的结构.当一个问题具有最优子结构时,提示我们动态规划可能会适用. 在寻找最优子结构时,可以遵循一种共同的模式:1)问题的一个解可以是一个选择:2)假设对于一个给定问题,已知的是一个可以导致最优解的选择:3)在已知这个选择后,要确定哪些子问题会随之发生. 最优子结构在问题域中以两种形式变化:1)有多少个子问题被使用在

【算法设计与分析基础】22、最优二叉查找树

package cn.xf.algorithm.ch08DynamicProgramming; import java.util.Arrays; import org.junit.Test; import cn.xf.algorithm.ch08DynamicProgramming.vo.ResultVo; /** * 最优二叉树问题 * * 思路:根据最优二叉树的问题就是查找对应的节点的比较次数的期望值保持最小 * 最优二叉查找树的期望搜索代价保持最小 * 例如:一颗树有节点{k0,k1,k2

最优二叉查找树_动态规划

原问题是给出各个节点和各个节点的被查找概率,然后构造一棵各个节点平均被查找比较次数最小的树,则该问题可以用动态规划来解决 示例如下 推广到一般的情况,并设T(i, j)是由记录{ri, …, rj}(1≤i≤j≤n)构成的二叉查找树,C(i, j)是这棵二叉查找树的平均比较次数,有下列分析 观察这个表,可知可知左边的表的第一行的第四列就是我们要求的最优平均比较次数,而右边的表我们可以知道在c(i ,j)得到最优解,即平均查找次数最小的根节点,比如一共四个节点,则我们从右边的R(1,4)的值即3是

算法导论之动态规划(最长公共子序列和最优二叉查找树)

动态规划师通过组合子问题的解而解决整个问题,将问题划分成子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解.和分治算法思想一致,不同的是分治算法适合独立的子问题,而对于非独立的子问题,即各子问题中包含公共的子子问题,若采用分治法会重复求解,动态规划将子问题结果保存在一张表中,避免重复子问题重复求解. 动态规划在多值中选择一个最优解,其算法设计一般分为4个步骤:描述最优解的结构:递归定义最优解的值:按自底向上的方式计算最优解的值:由计算出的结果构造一个最优解. 1)装配线调度 求解最快

【读书笔记】C Primer Plus ch.15位运算 示例程序15.3 定义并使用字段

通常,把位字段作为一种更紧凑储存数据的方式. 程序清单 15.3 fields.c 1 #include <stdio.h> 2 #include <stdbool.h> 3 // 线的样式 4 #define SOLID 0 // 实线 5 #define DOTTED 1 // 点线 6 #define DASHED 2 // 虚线 7 // 三原色 8 #define BLUE 4 9 #define GREEN 2 10 #define RED 1 11 // 混合色 12

[CLRS][CH 32]字符串匹配

问题简介 对于给定文本 T[n] 和模式 P[m],找到一个位移量 s,使得 T[s + j] = P[j] (0 <= s <= n-m, 1 <= j <= m),则说明模式 P 在文本 T 中出现且位移为 s.所以字符串问题就是在给定文本 T 中,找出指定模式 P 出现的所有有效位移的问题. 记号和术语 长度为0的空字符串用 ? 表示.字符串 x 的长度用 |x| 表示.字符串 x, y 的连接表示为xy,长度为 |x|+|y|.对于字符串 x, y, w,如果 x = wy

[CLRS][CH 20] van Emde Boas 树

vEB树简介 当关键字是有界范围内整数时,能够避免排序的 Ω(nlgn) 的下界限制.vEB树支持动态集合上运行时间为 O(lglgn) 的操作:Search, Insert, Delete, Min, Max, Successor 和 Predecessor. 接下来,用 n 表示集合中当前元素的个数,用 u 表示元素的可能取值范围.这样vEB树的操作运行时间就为 O(lglgu). 太难了...放弃先......