動態規劃--最優二叉樹問題
設 S={x1, x2, ···, xn} 是一個有序集合,且x1, x2, ···, xn表示有序集合的二叉搜尋樹利用二叉樹的頂點儲存有序集中的元素,而且具有性質:儲存於每個頂點中的元素x 大於其左子樹中任一個頂點中儲存的元素,小於其右子樹中任意頂點中儲存的元素。二叉樹中的葉頂點是形如(xi, xi+1) 的開區間。在表示S的二叉搜尋樹中搜索一個元素x,返回的結果有兩種情形:
(1) 在二叉樹的內部頂點處找到: x = xi
(2) 在二叉樹的葉頂點中確定: x∈ (xi , xi+1)
設在情形(1)中找到元素x = xi的概率為pi;在情形(2)中確定x∈ (xi
集合{q0,p1,q1,……pn,qn}稱為集合S的存取概率分佈。
最優二叉搜尋樹:在一個表示S的二叉樹T中,設儲存元素xi的結點深度為ci;葉結點(xj,xj+1)的結點深度為dj。
注:在檢索過程中,每進行一次比較,就進入下面一層,對於成功的檢索,比較的次數就是所在的層數加1。對於不成功的檢索,被檢索的關鍵碼屬於那個外部結點代表的可能關鍵碼集合,比較次數就等於此外部結點的層數。對於圖的內結點而言,第0層需要比較操作次數為1,第1層需要比較2次,第2層需要3次。
w表示在二叉搜尋樹T中作一次搜尋所需的平均比較次數。w又稱為二叉搜尋樹T的平均路長,在一般情況下,不同的二叉搜尋樹的平均路長是不同的。對於有序集S及其存取概率分佈{q
對於有n個關鍵碼的集合,其關鍵碼有n!種不同的排列,可構成的不同二叉搜尋樹有棵。(n個結點的不同二叉樹,卡塔蘭數)。如何評價這些二叉搜尋樹,可以用樹的搜尋效率來衡量。例如:識別符號集{1, 2, 3}={do, if, stop}可能的二分檢索樹為:
若P1=0.5, P2=0.1, P3=0.05,q0=0.15, q1=0.1, q2=0.05, q3=0.05,求每棵樹的平均比較次數(成本)。
Pa(n)=1 × p1 + 2 × p2+3 × p3 + 1×q0 +2×q1+ 3×( q2 + q3 ) =1 × 0.5+ 2 × 0.1+3 ×0.05 + 1×0.05 +2×0.1+ 3×( 0.05 + 0.05 ) =1.5
Pb(n)=1 × p1 + 2 × p3+3 × p2 + 1×q0 +2×q3 + 3×( q1 + q2 ) =1 × 0.5+ 2 × 0.05 + 3 ×0.1 + 1×0.15 +2×0.05+ 3×( 0.1 + 0.05 ) =1.6
Pc(n)=1 × p2 + 2 × (p1 + p3) + 2×(q0 +q1 +q2 + q3 ) =1 × 0.1+ 2 × (0.5 + 0.05) + 2×(0.15 + 0.1 + 0.05 + 0.05) =1.9
Pd(n)=1 × p3 + 2 × p1+3 × p2 + 1 × q3+2 × q0 +3 × (q1+ q2) =1 × 0.05 + 2 × 0.5 + 3 × 0.1 + 1×0.05 + 2 × 0.15 + 3 × (0.1 + 0.05) =2.15
Pe(n)=1 × p3 + 2 × p2+3 × p1 + 1 × q3+2 × q2 +3 × (q0 + q1) =1 × 0.05 + 2 × 0.1+ 3 × 0.5 + 1×0.05 + 2 × 0.15 + 3 × (0.15 + 0.1) =2.85
因此,上例中的最小平均路長為Pa(n)=1.5。
可以得出結論:結點在二叉搜尋樹中的層次越深,需要比較的次數就越多,因此要構造一棵最小二叉樹,一般儘量把搜尋概率較高的結點放在較高的層次.
2.動態規劃求解過程
1)最優二叉查詢樹的結構
如果一棵最優二叉查詢樹T有一棵包含關鍵字ki,……,kj的子樹T',那麼這棵子樹T’對於對於關鍵字ki,……kj和虛擬鍵di-1,……,dj的子問題也必定是最優的。
2)一個遞迴解
定義e[i,j]為搜尋一棵包含關鍵字ki,……,kj的最優二叉查詢樹的期望代價,則分類討論如下:
當j=i-1時,說明此時只有虛擬鍵di-1,故e[i,i-1] = qi-1
當j≥i時,需要從ki,……,kj中選擇一個跟kr,然後用關鍵字ki,……,kr-1來構造一棵最優二叉查詢樹作為左子樹,用關鍵字kr+1,……,kj來構造一棵最優二叉查詢樹作為右子樹。定義一棵有關鍵字ki,……,kj的子樹,定義概率的總和為:
因此如果kr是一棵包含關鍵字ki,……,kj的最優子樹的根,則有:
故e[i,j]重寫為:
最終的遞迴式如下:
3)計算一棵最優二叉查詢樹的期望搜尋代價
將e[i,j]的值儲存到一個二維陣列e[1..1+n,0..n]中,用root[i,j]來記錄關鍵字ki,……,kj的子樹的根,採用二維陣列root[1..n,1..n]來表示。為了提高效率,防止重複計算,需要個二維陣列w[1..n+1,0...n]來儲存w(i,j)的值,其中w[i,j] = w[i,j-1]+pj+qj。陣列給出了計算過程的虛擬碼:
1 OPTIMAL_BST(p,q,n)
2 for i=1 to n+1 //初始化e和w的值
3 do e[i,i-1] = qi-1;
4 w[i,i-1] = qi-1;
5 for l=1 to n
6 do for i=1 to n-l+1
7 do j=i+l-1;
8 e[i,j] = MAX;
9 w[i,j] = w[i,j-1]+pj+qj;
10 for r=i to j
11 do t=e[i,r-1]+e[r+1,j]+w[i,j]
12 if t<e[i,j]
13 then e[i,j] = t;
14 root[i,j] = r;
15 return e and root;
4)構造一棵最優二叉查詢樹
根據地第三步中得到的root表,可以遞推出各個子樹的根,從而可以構建出一棵最優二叉查詢樹。從root[1,n]開始向下遞推,一次找出樹根,及左子樹和右子樹。
3.程式設計實現
針對一個具體的例項程式設計實現,現在有5個關鍵字,其出現的概率P={0.15,0.10,0.05,0.10,0.20},查詢虛擬鍵的概率q={0.05,0.10,0.05,0.05,0.05,0.10}。採用C++語言是實現如下:
head.h
#include <iostream>
using namespace std;
#define N 5
#define MaxValue 1000000
void opimal_bst(float *p, float *q,float e[N+2][N+1],float w[N+2][N+1],int root[N+1][N+1]);
void construct_optimal_bst(int root[N+1][N+1],int i,int j);
void construct_optimal_bst_detail(int root[N+1][N+1],int i,int j);
main.cpp
#include "head.h"
void main()
{
float p[N+1]={0,0.15,0.1,0.05,0.1,0.2};
float q[N+1]={0.05,0.1,0.05,0.05,0.05,0.1};
float e[N+2][N+1];
float w[N+2][N+1];
int root[N+1][N+1];
opimal_bst(p, q,e,w,root);
cout<<"e:"<<endl;
int i,j;
for(i=1;i<=N+1;i++)
{
for (j=i-1;j<=N;j++)
{
cout<<e[i][j]<<" ";
}
cout<<endl;
}
cout<<"the cost of best binary tree is"<<e[1][N]<<endl;
cout<<"w:"<<endl;
for(int i=1;i<=N+1;i++)
{
for (int j=i-1;j<=N;j++)
{
cout<<w[i][j]<<" ";
}
cout<<endl;
}
cout<<"root:"<<endl;
for(i=1;i<=N;i++)
{
for(j=i;j<=N;j++)
{
cout<<root[i][j]<<" ";
}
cout<<endl;
}
construct_optimal_bst(root,1,N);
construct_optimal_bst_detail(root,1,N);
cout<<endl;
}
void opimal_bst(float *p, float *q,float e[N+2][N+1],float w[N+2][N+1],int root[N+1][N+1])
{
int i,j,l,r;
float 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]=MaxValue;
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;
}
}
}
}
}
void construct_optimal_bst(int root[N+1][N+1],int i,int j)
{
if (i<=j)
{
cout<<root[i][j]<<" ";
construct_optimal_bst(root,i,root[i][j]-1);
construct_optimal_bst(root,root[i][j]+1,j);
}
}
void construct_optimal_bst_detail(int root[N+1][N+1],int i,int j)
{
if (i==1&&j==N)
{
cout<<"k"<<root[1][N]<<"is root"<<endl;
}
if (i<j)
{
int r=root[i][j];
if (r!=i)
{
cout<<"k"<<root[i][r-1]<<"is left child of "<<"k"<<r<<endl;
}
construct_optimal_bst_detail(root,i,r-1);
if (r!=j)
{
cout<<"k"<<root[r+1][j]<<"is right child of "<<"k"<<r<<endl;
}
construct_optimal_bst_detail(root,r+1,j);
}
if (i==j)
{
cout<<"d"<<i-1<<"is left child of "<<"k"<<i<<endl;
cout<<"d"<<i<<"is right child of "<<"k"<<i<<endl;
}
if (i>j)
{
cout<<"d"<<j<<"is right child of "<<"k"<<j<<endl;
}
}
執行結果為
演算法OptimalBinarySearchTree中用s[i][j]儲存最優子樹T(i,j)的根節點中的元素。當s[i][n]=k時,xk為所求二叉搜尋樹根節點元素。其左子樹為T(1,k-1)。因此,i=s[1][k-1]表示T(1,k-1)的根節點元素為xi。依次類推,容易由s記錄的資訊在O(n)時間內構造出所求的最優二叉搜尋樹。
4.複雜度分析與優化:
演算法中用到3個數組e,w和root,故所需空間複雜度為O(n^2)。演算法的主要計算量在於計算。對於固定的r,它需要的計算時間O(j-i+1)=O(r+1)。因此演算法所耗費的總時間為:。
參考: