动态规划——最优二叉查找树问题(C代码实现)
引入
假设你要把一本英文书翻译成中文,翻译的过程中,你需要在词汇库中寻找每一个单词。假设每个词都已经按照字母序排列好,用二分法寻找单词,这个过程可以被抽象成一棵二叉查找树。
但是我们可能会遇到这种情况:一个单词出现频率特别高但它离根节点特别远。另一个词出现频率特别低,但它离根节点特别近,这样会导致搜索过程地比较次数大幅增加。除此之外,有一些生僻词可能并没有出现在你的词汇库中,每次遇到这些词的时候你都需要经过最差的比较次数。
为了减少搜索过程中的比较,我们可以把词汇出现的频率列入二叉树建立的考察中。
下面对这个问题进行形式化定义:
给定一个
n
n
n个不同关键字的已排序序列
K
=
<
k
1
,
k
2
,
…
,
k
n
>
K=<k_1,k_2,…,k_n>
K=<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
K
K中,因此我们还有
n
+
1
n+1
n+1个“伪关键字”
d
0
,
d
1
,
d
2
,
…
,
d
n
d_0,d_1,d_2,…,d_n
d0,d1,d2,…,dn表示不在
K
K
K中的值。
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表示其搜索概率。每次搜索要么成功(找到某个关键字
k
i
k_i
ki)要么失败(找到某个伪关键字
d
i
d_i
di),因此有如下公式:
∑
i
=
1
n
p
i
+
∑
i
=
0
n
q
i
=
1
\sum_{i=1}^{n}p_i+\sum_{i=0}^{n}q_i=1
i=1∑npi+i=0∑nqi=1
由于我们知道每个关键字和伪关键字的搜索概率,因而可以确定在一棵给定的二叉搜索树
T
T
T中进行一次搜索的期望代价为:
E
[
T
中
搜
索
代
价
]
=
∑
i
=
1
n
(
d
e
p
t
h
T
(
k
i
)
+
1
)
×
p
i
+
∑
i
=
0
n
(
d
e
p
t
h
T
(
d
i
)
+
1
)
×
q
i
=
1
+
∑
i
=
1
n
d
e
p
t
h
T
(
k
i
)
×
p
i
+
∑
i
=
0
n
d
e
p
t
h
T
(
d
i
)
×
q
i
E[T中搜索代价]=\sum_{i=1}^{n}(depth_T(k_i)+1)×p_i+\sum_{i=0}^{n}(depth_T(d_i)+1)×q_i=1+\sum_{i=1}^{n}depth_T(k_i)×p_i+\sum_{i=0}^{n}depth_T(d_i)×q_i
E[T中搜索代价]=i=1∑n(depthT(ki)+1)×pi+i=0∑n(depthT(di)+1)×qi=1+i=1∑ndepthT(ki)×pi+i=0∑ndepthT(di)×qi
d
e
p
t
h
T
表
示
某
个
结
点
在
树
T
中
的
深
度
depth_T表示某个结点在树T中的深度
depthT表示某个结点在树T中的深度
最优二叉搜索树不一定是高度最矮的,频率最高的关键字也未必是一个最优二叉搜索树的根节点。如果用暴力枚举法来计算,
n
n
n个结点能产生的二叉树的数量为
Ω
(
4
n
/
n
3
/
2
)
\Omega(4^n/n^{3/2})
Ω(4n/n3/2),因此我们采用动态规划法来解决这个问题。
用动态规划法解决最优二叉查找树问题
优化子结构
如果一颗最优二叉树 T T T有一颗包含关键字 k i , … , k j k_i,…,k_j ki,…,kj的子树 T ′ T' T′,那 T ′ T' T′必然是包含关键字 k i , … , k j k_i,…,k_j ki,…,kj和伪关键字 d i − 1 , … , d j d_{i-1},…,d_j di−1,…,dj的子问题的最优解。
证明: 如果存在一颗子树 T ′ ′ T'' T′′包含和 T ′ T' T′相同的元素且期望搜索代价更低,那么我们可以把 T ′ T' T′的位置用 T ′ ′ T'' T′′替换掉,从而使 T T T的期望搜索代价更低。
下面我们假设在由 k i , … , k j k_i,…,k_j ki,…,kj这串序列构成的树中,选取 k r k_r kr作为根结点能使之成为最优二叉查找树。那么 k r k_r kr的左子树中就包括 k i , … , k r − 1 k_i,…,k_{r-1} ki,…,kr−1及伪关键字 d i − 1 , … , d r − 1 d_{i-1},…,d_{r-1} di−1,…,dr−1,它的右子树中就包括 k r + 1 , … , k j k_{r+1},…,k_j kr+1,…,kj及伪关键字 d r , … , d j d_{r},…,d_{j} dr,…,dj。只要我们检查所有可能的根节点 k r ( i ≤ r ≤ j ) k_r(i≤r≤j) kr(i≤r≤j),并对每种情况分别求解包含 k i , … , k r − 1 k_i,…,k_{r-1} ki,…,kr−1及包含 k r + 1 , … , k j k_{r+1},…,k_j kr+1,…,kj的最优二叉查找树,即可保证找到原问题的最优解。
递归求解方案
我们先定义两个二维数组, e [ i , j ] e[i,j] e[i,j]为由从 i i i到 j j j的结点构成的二叉搜索树进行一次搜索的期望代价, w [ i , j ] w[i,j] w[i,j]为从 i i i到 j j j的结点的概率之和。最后我们希望求得的是 e [ 1 , n ] e[1,n] e[1,n]。
注意: w [ i , j ] = ∑ l = i j p l + ∑ l = i − 1 j q l w[i,j]=\sum_{l=i}^{j}p_l+\sum_{l=i-1}^{j}q_l w[i,j]=∑l=ijpl+∑l=i−1jql
先来考虑递归到底的情况,递归到最底的时候肯定只有一个关键字。但值得注意的是在这个问题中还有伪关键字,因此我们需要考虑在左右范围的下标(
i
,
j
i,j
i,j)是什么情况下时构造出来的子树只有一个伪关键字的结点。
前面提到在由
k
i
,
…
,
k
j
k_i,…,k_j
ki,…,kj这串序列构成的树中,选取
k
r
k_r
kr作为根结点能使之成为最优二叉查找树。那么
k
r
k_r
kr的左子树中就包括
k
i
,
…
,
k
r
−
1
k_i,…,k_{r-1}
ki,…,kr−1及伪关键字
d
i
−
1
,
…
,
d
r
−
1
d_{i-1},…,d_{r-1}
di−1,…,dr−1,它的右子树中就包括
k
r
+
1
,
…
,
k
j
k_{r+1},…,k_j
kr+1,…,kj及伪关键字
d
r
,
…
,
d
j
d_{r},…,d_{j}
dr,…,dj。那么当这个左子树只有一个伪关键字的时候显然
r
=
i
r=i
r=i,此时它的左子树就应该是
k
i
,
…
,
k
i
−
1
k_i,…,k_{i-1}
ki,…,ki−1,即并没有关键字结点。则再对这个左子树寻找最优子树时,
(
n
e
w
)
i
=
(
o
l
d
)
i
,
j
=
(
o
l
d
)
i
−
1
(new)i=(old)i,j=(old)i-1
(new)i=(old)i,j=(old)i−1。由此我们就知道了递归到底的情况是
j
=
i
−
1
j=i-1
j=i−1。即
j
=
i
−
1
j=i-1
j=i−1时
e
[
i
,
j
]
=
q
i
−
1
e[i,j]=q_{i-1}
e[i,j]=qi−1(其实是
q
i
−
1
∗
1
q_{i-1}*1
qi−1∗1,只有结点的时候只用比较一次,这里把
∗
1
*1
∗1省略掉了)。
那么对于
j
≥
i
j≥i
j≥i的情况呢?
首先想到的是
e
[
i
,
j
]
e[i,j]
e[i,j]等于它的两个子树的期望简单相加之后再加上树根的概率,但其实当我们在两棵子树上新找到一个结点作为它们的根的时候,这两个子树的每一个节点的深度都加了
1
1
1,也就是说去搜索子树里的节点的比较次数全多了一次,即还得再加上一次所有结点的概率(和刚才
q
i
−
1
∗
1
q_{i-1}*1
qi−1∗1省略了
∗
1
*1
∗1一样,这里多比较了一次,体现在公式上就是再补加上全体结点的概率)。因此
j
≥
i
j≥i
j≥i时
e
[
i
,
j
]
=
e
[
i
,
r
−
1
]
+
e
[
r
+
1
,
j
]
+
p
r
+
w
[
i
,
r
−
1
]
+
w
[
r
+
1
,
j
]
e[i,j]=e[i,r-1]+e[r+1,j]+p_r+w[i,r-1]+w[r+1,j]
e[i,j]=e[i,r−1]+e[r+1,j]+pr+w[i,r−1]+w[r+1,j]
e [ i , r − 1 ] + e [ r + 1 , j ] + p r e[i,r-1]+e[r+1,j]+p_r e[i,r−1]+e[r+1,j]+pr:它的两个子树的期望简单相加之后再加上树根的概率
w [ i , r − 1 ] + w [ r + 1 , j ] w[i,r-1]+w[r+1,j] w[i,r−1]+w[r+1,j]:因为深度加深了1,再补加上了一次所有结点的概率
而我们很容易发现:
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]
据此我们终于可以写出递归公式:
e
[
i
,
j
]
=
{
q
i
−
1
,
if
j
=
i
−
1
e
[
i
,
j
]
=
e
[
i
,
r
−
1
]
+
e
[
r
+
1
,
j
]
+
w
[
i
,
j
]
,
if
j
≥
i
e[i,j]= \begin{cases} q_{i-1}, & \text {if $j=i-1$ } \\e[i,j]=e[i,r-1]+e[r+1,j]+w[i,j], & \text{if $j≥i$} \end{cases}
e[i,j]={qi−1,e[i,j]=e[i,r−1]+e[r+1,j]+w[i,j],if j=i−1 if j≥i
C代码填表
void OPTIMAL_BST(int p[],int q[],int n,int **e,int **w,int **root)
{
int i,j,l,r,t;
for(i=1;i<=n+1;i++)
{
e[i][i-1]=q[i-1];
w[i][i-1]=q[i-1];
}
for(l=1;l<=n;l++)
{
for(i=1;i<=n-l+1;i++)
{
j=i+l-1;
e[i][j]=INT_MAX;
w[i][j]=w[i][j-1]+p[j]+q[j];
for(r=i;r<=j;r++)
{
t=e[i][r-1]+e[r+1][j]+w[i][j];
if(t<e[i][j])
{
e[i][j]=t;
root[i][j]=r;
}
}
}
}
return;
}