前文分别介绍了《动态规划算法求解最长公共子序列》和《动态规划算法求解0-1背包》,本文将通过最优二叉搜索树问题的求解,带大家进一步熟悉动态规划算法,并给出C++代码实现。
1. 最优二叉搜索树
首先,我们来看一下什么是二叉搜索树,什么又是最优二叉搜索树。
1.1 二叉搜索树的定义
二叉搜索树T是一棵所有结点的元素值互异的二元树,它或者为空,或者其每个结点含有一个可以比较大小的数据元素,且有:
- T的左子树的所有元素比根结点中的元素小;
- T的右子树的所有元素比根结点中的元素大;
- T的左子树和右子树也是二叉搜索树。
1.2 二叉搜索树的搜索性能
根据二叉搜索树的特点,其进行检索的伪码如下:
SEARCH(T, X, i)
/* 在二叉搜索树T中检索X,如果X不在T中,则置i=0;否则有IDENT(i)=X */
i = T
while i != 0 do
case
:X < IDENT(i): i = LCHILD(i) //若X小于IDENT(i),则在左子树中继续查找
:X = IDENT(i): return //X等于IDENT(i),则返回
:X > IDENT(i): i = RCHILD(i) //若X大于IDENT(i),则在右子树中继续查找
end case
end while
end SEARCH
如下图所示,对于给定的标识符集合,可能构造出不同的二叉搜索树。二叉搜索树的形态不同,其搜索性能也会有所不同。
如图(a),最坏情况下查找标识符loop
需要进行
4
4
4次比较(依次和if
、while
、repeat
、loop
进行比较)。成功检索平均需要进行
12
5
\frac{12}{5}
512次比较(找到if
需要1次比较,找到for
和while
均需要2次比较,找到repeat
需要3次比较,找到loop
需要4次比较,平均检索次数
=
1
+
2
+
2
+
3
+
4
5
=
12
5
=\frac{1+2+2+3+4}{5}=\frac{12}{5}
=51+2+2+3+4=512)。
可以发现:二叉搜索树上每个结点被成功检索到需要的比较次数等于结点所在的层数。那么什么是最优二叉搜索树呢?
1.3 最优二叉搜索树的问题定义
我们看如下图所示的二叉搜索树,设给定标识符集合为 { a 1 , a 2 , … , a n } \{a_1, a_2, …, a_n\} {a1,a2,…,an},圆形表示二叉搜索树中实际存在的结点,代表成功检索的情况,共有 n n n个;方形表示二叉搜索树中的虚结点,代表不成功检索的情况,共有 ( n + 1 ) (n+1) (n+1)个,设为 { E 0 , E 1 , … E n } \{E_0, E_1, …E_n\} {E0,E1,…En}。其中, E 0 = { x ∣ x < a 0 } , E i = { x ∣ a i < x < a i + 1 ( 1 ≤ i < n ) } , E n = { x ∣ x > a n } E_0=\{x|x<a_0\},E_i=\{x|a_i<x<a_{i+1}(1\leq i < n)\},E_n=\{x|x>a_n\} E0={x∣x<a0},Ei={x∣ai<x<ai+1(1≤i<n)},En={x∣x>an}。
设每个实际存在的结点
a
i
a_i
ai成功检索的概率为
P
(
i
)
P(i)
P(i),每个虚结点
E
i
E_i
Ei不成功检索的概率为
Q
(
i
)
Q(i)
Q(i),那么二叉搜索树的
平均检索成本
=
∑
每种情况出现的概率
∗
该情况下需要比较的次数
=
∑
1
≤
i
≤
n
P
(
i
)
∗
l
e
v
e
l
(
a
i
)
+
∑
0
≤
i
≤
n
Q
(
i
)
∗
(
l
e
v
e
l
(
E
i
)
−
1
)
平均检索成本=\sum{每种情况出现的概率*该情况下需要比较的次数}=\sum_{1\leq i \leq n}{P(i)*level(a_i)}+\sum_{0\leq i \leq n}{Q(i)*(level(E_i)-1)}
平均检索成本=∑每种情况出现的概率∗该情况下需要比较的次数=∑1≤i≤nP(i)∗level(ai)+∑0≤i≤nQ(i)∗(level(Ei)−1)。
最优二叉搜索树的目标即是:在给定的标识符集合下,构造出一棵平均检索成本最小的二叉搜索树。下面我们来看下如何用动态规划算法进行求解。
2. 动态规划算法实现思路
构造二叉搜索树的过程可以看作一个多阶段决策过程,设树根为 a k a_k ak,那么它的左子树为 a 1 , a 2 , … , a k − 1 a_1, a_2, …, a_{k-1} a1,a2,…,ak−1及 E 0 , E 1 , … , E k − 1 E_0, E_1, …, E_{k-1} E0,E1,…,Ek−1构成的二叉搜索树,它的右子树为 a k + 1 , a k + 2 , … , a n a_{k+1}, a_{k+2}, …, a_n ak+1,ak+2,…,an及 E k , E k + 1 , … , E n E_k, E_{k+1}, …, E_n Ek,Ek+1,…,En构成的二叉搜索树。
那么左右子树的平均检索成本分别为
c
o
s
t
(
L
)
=
∑
1
≤
i
<
k
P
(
i
)
∗
l
e
v
e
l
(
a
i
)
+
∑
0
≤
i
<
k
Q
(
i
)
∗
(
l
e
v
e
l
(
E
i
)
−
1
)
cost(L)=\sum_{1\leq i < k}{P(i)*level(a_i)}+\sum_{0\leq i < k}{Q(i)*(level(E_i)-1)}
cost(L)=∑1≤i<kP(i)∗level(ai)+∑0≤i<kQ(i)∗(level(Ei)−1)
c
o
s
t
(
R
)
=
∑
k
<
i
≤
n
P
(
i
)
∗
l
e
v
e
l
(
a
i
)
+
∑
k
≤
i
≤
n
Q
(
i
)
∗
(
l
e
v
e
l
(
E
i
)
−
1
)
cost(R)=\sum_{k < i \leq n}{P(i)*level(a_i)}+\sum_{k \leq i \leq n}{Q(i)*(level(E_i)-1)}
cost(R)=∑k<i≤nP(i)∗level(ai)+∑k≤i≤nQ(i)∗(level(Ei)−1)
注意,上述 c o s t ( L ) cost(L) cost(L)与 c o s t ( R ) cost(R) cost(R)中每个结点的级数是相对于子树的根测定,与原树的根相比少了1级。
记 W ( i , j ) = Q ( i ) + ∑ i + 1 ≤ l ≤ j ( P ( l ) + Q ( l ) ) W(i, j)=Q(i)+\sum_{i+1 \leq l \leq j}(P(l)+Q(l)) W(i,j)=Q(i)+∑i+1≤l≤j(P(l)+Q(l)),则原二叉搜索树的平均检索成本为 c o s t ( T ) = P ( k ) + c o s t ( L ) + c o s t ( R ) + W ( 0 , k − 1 ) + W ( k , n ) cost(T)=P(k)+cost(L)+cost(R)+W(0, k-1)+W(k, n) cost(T)=P(k)+cost(L)+cost(R)+W(0,k−1)+W(k,n)。
记由 a i + 1 , a i + 2 , … , a j a_{i+1}, a_{i+2}, …, a_j ai+1,ai+2,…,aj及 E i , E i + 1 , … , E j E_i, E_{i+1}, …, E_j Ei,Ei+1,…,Ej构成的二叉搜索树的成本为 C ( i , j ) C(i, j) C(i,j),则有 c o s t ( L ) = C ( 0 , k − 1 ) , c o s t ( R ) = C ( k , n ) cost(L)=C(0, k-1),cost(R)=C(k, n) cost(L)=C(0,k−1),cost(R)=C(k,n)。
则,
C
(
0
,
n
)
=
min
1
≤
k
≤
n
{
C
(
0
,
k
−
1
)
+
C
(
k
,
n
)
+
P
(
k
)
+
W
(
0
,
k
−
1
)
+
W
(
k
,
n
)
}
C(0, n)=\min_{1 \leq k \leq n} \{C(0, k-1)+C(k, n)+P(k)+W(0,k-1)+W(k,n)\}
C(0,n)=min1≤k≤n{C(0,k−1)+C(k,n)+P(k)+W(0,k−1)+W(k,n)}。
对任意
C
(
i
,
j
)
C(i, j)
C(i,j)有,
C
(
i
,
j
)
=
min
i
<
k
≤
j
{
C
(
i
,
k
−
1
)
+
C
(
k
,
j
)
+
P
(
k
)
+
W
(
i
,
k
−
1
)
+
W
(
k
,
j
)
}
=
min
i
<
k
≤
j
{
C
(
i
,
k
−
1
)
+
C
(
k
,
j
)
}
+
W
(
i
,
j
)
C(i, j)=\min_{i < k \leq j} \{C(i, k-1)+C(k, j)+P(k)+W(i,k-1)+W(k,j)\}=\min_{i < k \leq j} \{C(i, k-1)+C(k, j)\}+W(i, j)
C(i,j)=mini<k≤j{C(i,k−1)+C(k,j)+P(k)+W(i,k−1)+W(k,j)}=mini<k≤j{C(i,k−1)+C(k,j)}+W(i,j)。
递推过程如下:
-
初始值: { C ( i , i ) = 0 W ( i , i ) = Q ( i ) 0 ≤ i ≤ n \left\{\begin{matrix} C(i,i)=0 \\ W(i,i)=Q(i) & 0 \leq i \leq n \end{matrix}\right. {C(i,i)=0W(i,i)=Q(i)0≤i≤n
-
首先计算所有 j − i = 1 j-i=1 j−i=1的 C ( i , j ) C(i,j) C(i,j)。
-
然后依次计算 j − i = 2 , 3 , … , n j-i=2,3,…,n j−i=2,3,…,n的 C ( i , j ) C(i,j) C(i,j)。
-
C ( 0 , n ) = C(0,n)= C(0,n)=最优二叉搜索树的成本。
3. 程序代码
以下C++代码对上述思路进行了代码实现,同时使用括号化结构格式化输出最终得到的最优二叉搜索树。
//动态规划求解最优二叉搜索树
#include <iostream>
using namespace std;
const int MAX = 10000000;
typedef struct TNode { //定义二叉树的结构
int elem; //结点的值
struct TNode *left; //左孩子
struct TNode * right; //右孩子
}TNode;
//动态规划求解最优二叉树,p[]储存实际结点的概率,q[]储存虚键的概率,n为实际结点的个数
//e[i][j]记录当前搜索期望,w[i][j]记录每个子树上的概率和,root[i][j]记录树Tij的根
void optimal_bst(double p[], double q[], int n, double **e, double **w, int **root) {
for (int i = 1; i <= n + 1; i++) { //虚键的情况
e[i][i - 1] = q[i - 1];
w[i][i - 1] = q[i - 1];
}
for (int l = 1; l <= n; l++) { //将实际结点的个数逐渐从1扩展到n
for (int i = 1; i <= n - l + 1; i++) {
int j = i + l - 1;
e[i][j] = MAX; //先赋值无穷
w[i][j] = w[i][j - 1] + p[j] + q[j];
for (int r = i; r <= j; r++) {
double tmp = e[i][r - 1] + e[r + 1][j] + w[i][j];
if (tmp < e[i][j]) { //求 min{e[i][r-1] + e[r+1][j] + w[i][j]}
e[i][j] = tmp;
root[i][j] = r;
}
}
}
}
}
//递归输出最优二叉搜索树的括号化结构,虚拟键不输出,只输出实际结点
void print_optimal_bst(int i, int j, int **root, int nodeValue[]) {
if (i > j) {
cout << "{}";
return;
}
else {
int r = root[i][j];
cout << "K" << r << "(" << nodeValue[r] << ")";
cout << "{";
print_optimal_bst(i, r - 1, root, nodeValue);
cout << ",";
print_optimal_bst(r + 1, j, root, nodeValue);
cout << "}";
}
}
//输出上述括号化结构的解释,并构建最优二叉搜索树
void construct_optimal_bst(int i, int j, int k, int n, int **root, int nodeValue[], TNode *T) {
int r = root[i][j];
if (i == 1 && j == n) { //二叉树的根
cout << "K" << r << ": root" << endl;
T->elem = nodeValue[r]; //将根存入树结构中
construct_optimal_bst(1, r - 1, r, n, root, nodeValue, T);
construct_optimal_bst(r + 1, n, r, n, root, nodeValue, T);
}
else if (j == i - 1) { //遇到虚拟键
if (j < k) {
cout << "D" << j << ": Left child of K" << k << endl;
}
else {
cout << "D" << j << ": Right child of K" << k << endl;
}
}
else { //遇到实际结点
if (nodeValue[r] < nodeValue[k]) {
cout << "K" << r << ": Left child of K" << k << endl;
TNode *tl = new TNode; //为T的左孩子开空间
if (tl == NULL) {
cout << "Memory allocation error!";
exit(-1);
}
tl->left = NULL;
tl->right = NULL;
tl->elem = nodeValue[r];
T->left = tl;
construct_optimal_bst(i, r - 1, r, n, root, nodeValue, T->left); //左子树递归调用
construct_optimal_bst(r + 1, j, r, n, root, nodeValue, T->left);
}
else {
cout << "K" << r << ": Right child of K" << k << endl;
TNode *tr = new TNode; //为T的右孩子开空间
if (tr == NULL) {
cout << "Memory allocation error!";
exit(-2);
}
tr->left = NULL;
tr->right = NULL;
tr->elem = nodeValue[r];
T->right = tr;
construct_optimal_bst(i, r - 1, r, n, root, nodeValue, T->right); //右子树递归调用
construct_optimal_bst(r + 1, j, r, n, root, nodeValue, T->right);
}
}
}
//在二叉搜索树T中查找元素elem,输出查找结果以及比较的次数count
void binarySearch(int elem, TNode *T, int count) {
count++;
if (T == NULL) { //结果查找失败
cout << "NULL" << endl;
cout << "After " << count << " comparisons, query failed." << endl;
}
else if (T->elem == elem) { //结果查找成功
cout << T->elem << endl;
cout << "After " << count << " comparisons, query succeeded!" << endl;
}
else if (elem < T->elem) { //若elem < T->elem,则在左子树继续查找
cout << T->elem << " --> ";
binarySearch(elem, T->left, count);
}
else if (elem > T->elem) { //若elem > T->elem,则在右子树继续查找
cout << T->elem << " --> ";
binarySearch(elem, T->right, count);
}
}
int main() {
int n = 7; //实际结点的个数
int nodeValue[] = {0, 2, 5, 8, 16, 23, 29, 35}; //各结点的值
double p[] = {0, 13, 17, 25, 24, 19, 15, 12}; //查找成功的概率
double q[] = {8, 9, 1, 10, 17, 11, 13, 7}; //查找失败的概率
//为e,w,root三表开空间
double **e = new double*[n + 2];
for (int i = 0; i < n + 2; i++) {
e[i] = new double[n + 2];
}
double **w = new double*[n + 2];
for (int i = 0; i < n + 2; i++) {
w[i] = new double[n + 2];
}
int **root = new int*[n + 2];
for (int i = 0; i < n + 2; i++) {
root[i] = new int[n + 2];
}
//求解最优二叉搜索树的代价
optimal_bst(p, q, n, e, w, root);
//输出最优二叉搜索树的代价
cout << "The cost of the optimal binary tree is " << e[1][n] << endl;
//输出最优二叉搜索树的括号化结构
print_optimal_bst(1, n, root, nodeValue);
cout << endl;
//为二叉搜索树T开空间
TNode *T = new TNode;
if (T == NULL) {
cout << "Memory allocation error!" << endl;
exit(-3);
}
T->left = NULL;
T->right = NULL;
T->elem = nodeValue[n + 1];
//构建最优二叉搜索树,并输出详细结构
construct_optimal_bst(1, n, root[1][n], n, root, nodeValue, T);
//查找元素elem
int elem;
while (1) {
cout << endl;
cout << "Please enter the element you want to look for (Enter -1 to quit): ";
cin >> elem;
if (elem == -1) {
break;
}
else {
int count = 0;
binarySearch(elem, T, count); //返回是否查找成功,经过了几次比较
}
}
return 0;
}
4. 运行结果
给定实际节点数:7,实际节点:{2, 5, 8, 16, 23, 29, 35},对应概率(放大了N倍):{13, 17, 25, 24, 19, 15, 12},查找失败概率{8, 9, 1, 10, 17, 11, 13, 7},运行程序,程序首先输出最优二叉搜索树的代价,然后输出最优二叉搜索树的括号化结构,同一级大括号内的结点在同一层,小括号中给出结点的实际值。接着输出对括号化结构的解释,最后提示用户输入想要查找的元素,程序给出查找结果及比较次数。