1、前言:
接着學習動態規划方法,最優二叉查找樹問題。二叉查找樹參考http://www.cnblogs.com/Anker/archive/2013/01/28/2880581.html。如果在二叉樹中查找元素不考慮概率及查找不成功的情況下,可以采用紅黑樹或者平衡二叉樹來搜索,這樣可以在O(lgn)時間內完成。而現實生活中,查找的關鍵字是有一定的概率的,就是說有的關鍵字可能經常被搜索,而有的很少被搜索,而且搜索的關鍵字可能不存在,為此需要根據關鍵字出現的概率構建一個二叉樹。比如中文輸入法字庫中各詞條(單字、詞組等)的先驗概率,針對用戶習慣可以自動調整詞頻——所謂動態調頻、高頻先現原則,以減少用戶翻查次數,使得經常用的詞匯被放置在前面,這樣就能有效地加快查找速度。這就是最優二叉樹所要解決的問題。
2、問題描述
給定一個由n個互異的關鍵字組成的有序序列K={k1<k2<k3<,……,<kn}和它們被查詢的概率P={p1,p2,p3,……,pn},要求構造一棵二叉查找樹T,使得查詢所有元素的總的代價最小。對於一個搜索樹,當搜索的元素在樹內時,表示搜索成功。當不在樹內時,表示搜索失敗,用一個“虛葉子節點”來標示搜索失敗的情況,因此需要n+1個虛葉子節點{d0<d1<……<dn},對於應di的概率序列是Q={q0,q1,……,qn}。其中d0表示搜索元素小於k1的失敗結果,dn表示搜索元素大於kn的失敗情況。di(0<i<n)表示搜索節點在ki和k(i+1)之間時的失敗情況。因此有如下公式:
由每個關鍵字和每個虛擬鍵被搜索的概率,可以確定在一棵給定的二叉查找樹T內一次搜索的期望代價。設一次搜索的實際代價為檢查的節點個數,即在T內搜索所發現的節點的深度加上1。所以在T內一次搜索的期望代價為:
需要注意的是:一棵最優二叉查找樹不一定是一棵整體高度最小的樹,也不一定總是把最大概率的關鍵字放在根部。
(3)動態規划求解過程
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]開始向下遞推,一次找出樹根,及左子樹和右子樹。
4、編程實現
針對一個具體的實例編程實現,現在有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++語言是實現如下:
1 #include <iostream> 2 using namespace std; 3 #define N 5 4 #define MAX 999999.99999 5 void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]); 6 void construct_optimal_bst1(int root[N+1][N+1],int i,int j); 7 void construct_optimal_bst2(int root[N+1][N+1],int i,int j); 8 int main() 9 { 10 float p[N+1] = {0,0.15,0.10,0.05,0.10,0.20}; 11 float q[N+1] = {0.05,0.10,0.05,0.05,0.05,0.10}; 12 float e[N+2][N+1]; 13 int root[N+1][N+1]; 14 int i,j; 15 optimal_binary_search_tree(p,q,N,e,root); 16 cout<<"各個子樹的期望代價如下所示:"<<endl; 17 for(i=1;i<=N+1;i++) 18 { 19 for(j=i-1;j<=N;j++) 20 cout<<e[i][j]<<" "; 21 cout<<endl; 22 } 23 cout<<"最優二叉查找樹的代價為: "<<e[1][N]<<endl; 24 cout<<"各個子樹根如下表所示:"<<endl; 25 for(i=1;i<=N;i++) 26 { 27 for(j=i;j<=N;j++) 28 cout<<root[i][j]<<" "; 29 cout<<endl; 30 } 31 cout<<"構造的最優二叉查找樹如下所示:"<<endl; 32 construct_optimal_bst1(root,1,N); 33 cout<<"\n最優二叉查找樹的結構描述如下:"<<endl; 34 construct_optimal_bst2(root,1,N); 35 cout<<endl; 36 return 0; 37 } 38 void optimal_binary_search_tree(float *p,float *q,int n,float e[N+2][N+1],int root[N+1][N+1]) 39 { 40 int i,j,k,r; 41 float t; 42 float w[N+2][N+1]; 43 for(i=1;i<=N+1;++i) //主表和根表元素的初始化 44 { 45 e[i][i-1] = q[i-1]; 46 w[i][i-1] = q[i-1]; 47 } 48 for(k=1;k<=n;++k) //自底向上尋找最優子樹 49 for(i=1;i<=n-k+1;i++) 50 { 51 j = i+k-1; 52 e[i][j] = MAX; 53 w[i][j] = w[i][j-1]+p[j]+q[j]; 54 55 for(r=i;r<=j;r++) //找最優根 56 { 57 t = e[i][r-1] + e[r+1][j] +w[i][j]; 58 59 if(t < e[i][j]) 60 { 61 e[i][j] = t; 62 root[i][j] = r; 63 } 64 } 65 } 66 } 67 void construct_optimal_bst1(int root[N+1][N+1],int i,int j) 68 { 69 70 if(i<=j) 71 { 72 int r = root[i][j]; 73 cout<<r<<" "; 74 construct_optimal_bst1(root,i,r-1); 75 construct_optimal_bst1(root,r+1,j); 76 } 77 } 78 void construct_optimal_bst2(int root[N+1][N+1],int i,int j) 79 { 80 if(i==1 && j== N) 81 cout<<"k"<<root[1][N]<<"是根"<<endl; 82 if(i<j) 83 { 84 int r = root[i][j]; 85 if(r != i) 86 cout<<"k"<<root[i][r-1]<<"是k"<<r<<"的左孩子"<<endl; 87 construct_optimal_bst2(root,i,r-1); 88 if(r!= j) 89 cout<<"k"<<root[r+1][j]<<"是k"<<r<<"的右孩子"<<endl; 90 construct_optimal_bst2(root,r+1,j); 91 } 92 if(i==j) 93 { 94 cout<<"d"<<i-1<<"是k"<<i<<"左孩子"<<endl; 95 cout<<"d"<<i<<"是k"<<i<<"右孩子"<<endl; 96 } 97 if(i>j) 98 cout<<"d"<<j<<"是k"<<j<<"右孩子"<<endl; 99 }
程序測試結果如下所示:
參考資料:http://www.cnblogs.com/lpshou/archive/2012/04/26/2470914.html