《算法導論》動態規劃—最優二分搜索樹
案例
?假如我們現在在設計一個英文翻譯程序,要把英文翻譯成漢語,顯然我們需要知道每個單詞對應的漢語意思。我們可以建立一顆二分搜索樹來實現英語到漢語的關聯。為了更快速地翻譯,我們可以使用AVL樹或者紅黑樹使每次查詢的時間復雜度Θ(lgn),實際上對於字典翻譯程序來說這麽做存在一個問題,比如“the”這個單詞經常用,卻很有可能存在於十分遠離樹根的位置,而“machicolation”這種不常用的單詞很可能存在於十分靠近樹根的位置,這就導致查詢頻率高的單詞需要的查詢時間更長,而查詢頻率很低的單詞查詢時間卻很短。這明顯不符合我們的期望,我們希望高頻率的單詞用更少的時間查詢到,低頻率的單詞則用相對更長的時間查詢到,也就是高頻單詞靠近樹根,而低頻單詞遠離樹根。這時,我們就需要一顆最優二分搜索樹來解決這個問題。
最優二分搜索樹
何為最優二分搜索樹
給定一個組長度為n關鍵字的有序序列K=< k1, k2, k3 ···· kn >
以及對應關鍵字的出現概率P=< p1, p2, p3 ···· pn >
由於我們要檢索的關鍵字可能不在樹中,所以我們需要n+1個偽關鍵字
給定一組長度為n+1的偽關鍵字D=< d0, d1, d2 ···· dn >
其中d0表示小於任何一個關鍵字的值,dn表示大於任何一個關鍵字的值,當 i != 0 && i != n 時,di
表示所有大於ki且小於ki+1的不存在於序列K中的值的集合(不考慮值相等的情況)(比如d5就表示所有大於k5且小於k6的不存在於序列K中的值的集合)
若滿足下列條件則該二分搜索樹為一顆最優二分搜索樹
- 是一顆二分搜索樹(廢話)
- 滿足下列條件
註意
- 最優二分搜索樹不一定是一顆平衡二分搜索樹
- 最優二分搜索樹的根節點不一定是出現頻率最高的節點
例子
如何構建一顆最優二分搜索樹
窮舉法
?如果要一個一個地去窮舉出所有的形態再從中挑選時空復雜度很大,一個有n的節點的二叉樹的形態有很明顯如果窮舉的話是非常不劃算的。
動態規劃
是否可以使用
?既然窮舉法不行那麽動態規劃是否適用呢?使用動態規劃需要兩個條件
- 具有優化子結構
- 具有重疊子問題
?這裏可以直接給出最優二分搜索樹問題的優化子結構:對於一顆最優二分搜索樹T,其任意一顆子樹T1一定是一顆最優二分搜索樹。這裏可以用反證法證明,T是一顆最優二分搜索樹,如果存在一個子樹T1不是最優二分搜索樹,那麽將此子樹替換為包含關鍵字相同但是形態不相同的另外一顆子樹T2,就很有可能使的整棵樹T的期望搜索代價更低,這就不滿足T是一顆最優二分搜索樹的前提。
?根據上述的分析,可以知道最優二分搜索樹問題是存在重疊子問題的,因為如果我們要構造一顆最優二分搜索樹,必須優先構造其子樹,構造其子樹的時候要構造其子樹的子樹,如此下去,就存在了重疊的子問題。
?因此最優二分搜索樹問題是可以使用動態規劃求解的。
問題分析
?值得註意的是,如果i=j-1,這顆子樹實際上不包含任何實際的關鍵字,但是會包含為關鍵字di-1(所有小於ki的不存在於序列K中的數字的集合)。這個問題我們並沒有明顯的規律可循,所以我們只能尋找一個關鍵字kr(i <= r <= j),當kr作為該子樹的樹根時期望搜索代價最低,只能一個一個地試。
- 我們以COST[i][j]表示包含關鍵字 ki ···· kj 的子樹的搜索代價
- 我們以W[i][j]表示包含關鍵字 ki ···· kj 以及偽關鍵字 di-1 ···· dj的子樹的每個關鍵字和偽關鍵字的概率和
遞歸式
子問題求解順序(矩陣填充方式)
?子問題矩陣如圖
?應該按照對角線方式進行填充,因為每次求解一個對角線上的子問題,都需要前一個對角線的結果
本文涉及兩個矩陣,兩個矩陣的填充方式相同
C++代碼
#include <iostream>
#include <string>
#include <algorithm>
#include <map>
using namespace std;
const double INF = 99999;
void OPTIMAL_BST(double* p, double* q, int n);
int main()
{
int n;
cin >> n;
double* p = new double[n + 1];
double* q = new double[n];
for (int i = 1; i <= n; i++)
cin >> p[i];
for (int i = 0; i <= n; i++)
cin >> q[i];
OPTIMAL_BST(p, q, n);
delete p, q;
system("pause");
}
void OPTIMAL_BST(double* p, double* q, int n)
{
double** e = new double*[n + 2];
double** w = new double*[n + 2];
double t = 0.0;
//int** root = new int*[n + 2];
int j = 0;
for (int i = 0; i <= n + 1; i++)
{
e[i] = new double[n + 1];
w[i] = new double[n + 1];
//root[i] = new int[n + 1];
//矩陣置零
memset(e[i], 0, sizeof(double)*(n + 1));
memset(w[i], 0, sizeof(double)*(n + 1));
//memset(root[i], 0, sizeof(int)*(n + 1));
}
//填充第一個對角線
for (int i = 1; i <= n + 1; i++)
{
e[i][i - 1] = q[i - 1];
w[i][i - 1] = q[i - 1];
}
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n - k + 1; i++)
{
j = i + k - 1;
e[i][j] = INF;
w[i][j] = w[i][j - 1] + p[j] + q[j];
for (int r = i; r <= j; r++)
{
t = e[i][r - 1] + e[r + 1][j] + w[i][j];
if (e[i][j] - t > 0.0001)
{
e[i][j] = t;
//root[i][j] = r;
}
}
}
}
cout << endl << endl;
for (int i = 1; i <= n + 1; i++)
{
for (int j = 0; j <= n; j++)
cout << e[i][j] << ‘\t‘;
cout << endl << endl << endl;
}
delete e, w;
//delete root;
}
《算法導論》動態規劃—最優二分搜索樹