動態規劃(5)——算法導論(20)

1. 提出問題

假設咱們要設計一個將英語翻譯成法語的程序,即對英文文本中出現的每一個單詞,咱們須要查找其對應的法語單詞。爲了實現這一查找操做,咱們能夠構建一棵二叉搜索樹,將n個英文單詞做爲關鍵字,對應的法語單詞做爲關聯數據。html

經過使用紅黑樹或其餘平衡搜索樹結構,咱們能夠作到平均搜索時間爲O(lgn)。但須要注意到的是,每一個單詞的出現頻率是不同的,而且這種頻率的差別仍是比較大的。好比像 "the" 這種單詞,出現的頻率就比較高,而像 "machicolation" 這類單詞幾乎就不會出現。所以,若是咱們在不考慮單詞出現頻率的狀況下去構造二叉搜索樹,頗有可能將相似 "the" 這類高頻單詞置於樹的較深的結點處,這樣勢必會使搜索時間增長。node

因而,在給定單詞出現頻率的前提下,咱們應該如何去組織一棵二叉搜索樹,使得全部搜索操做訪問的結點總數最少呢?這即是二叉搜索樹問題(optimal binary search tree)。其形式化的描述以下:python

給定一個包含n個不一樣關鍵字且已排序的序列\(K = < k_1, k_2,...,k_n >(其中k_1<k_2<...<k_n)\)和關鍵字\(k_i\)的搜索頻率\(p_i,i=1,2...n\);還給定(n+1)個「僞關鍵字」\(d_0,d_1,...d_n\),表示不在K中的值(由於有些搜索值可能不在K中),同時也給出其被搜索的頻率\(q_i\)。要用這些關鍵字構造一棵二叉搜索樹T使得在T中搜索一次的指望搜索代價最小。算法

指望搜索代價E(T)可由以下公式計算出:數組

\[ E(T) = \sum_{i=1}^n(depth(k_i)+1)\cdot p_i+\sum_{i=0}^n(depth(d_i)+1)\cdot q_i \]spa

其中,depth(node)表示node的深度(約定根結點的深度爲0)。翻譯

因爲設計

\[ \sum_{i=1}^np_i + \sum_{i=0}^nq_i = 1 \]code

咱們能夠將上述E(T)計算式變形爲:htm

\[ E(T) = 1 + \sum_{i=1}^ndepth(k_i) \cdot p_i + \sum_{i=0}^n depth(d_i) \cdot q_i \]

舉個例子,對一個n = 5的關鍵字集合,給出以下搜索機率:

i 0 1 2 3 4 5
\(p_i\) 0.15 0.10 0.05 0.10 0.20
\(q_i\) 0.05 0.10 0.05 0.05 0.05 0.10

咱們能夠這麼構造出一棵二叉搜索樹:

方式a

也能夠這麼構造:

方式b

經過計算能夠發現,第一種方式的搜索代價爲2.8,而第二種方式的搜索代價爲2.75,所以第二種方式更好。事實上,第二種方式構造的二叉搜索樹是一棵最優搜索二叉樹。從該例子中咱們能夠看出,最優二叉搜索樹不必定是高度最矮的二叉樹,並且機率最大的關鍵字也不必定出如今根結點,不要把它和哈夫曼樹混淆。

PS:固然,上述採用二叉搜索樹的策略也許並非最好的,好比能夠採用效率更高的hashtable,好比採用分塊查找。可是,二叉搜索樹較hashtable而言,也有其優點,具體可參見 Advantages of BST over Hash Table。這裏只是以這個案例引出最優二叉搜索樹問題。

2. 分析問題

一樣,咱們試圖用動態規劃方法來解決該問題。老規矩,先考察動態規劃方法的兩個重要特性。

2.1 最優子結構

假定一棵最優二叉搜索樹T有一棵子樹T',那麼若是將T'單獨拿出來考慮,它必然也是一棵最優二叉搜索樹。咱們一樣能夠用剪切-粘貼法來證實這點:若是T'不是一棵最優二叉搜索樹,那麼咱們把有T'包含的關鍵字組成的最優二叉搜索樹」粘貼「到T'位置,此時構造的樹必定比以前二叉搜索樹T更優,這與T是最優二叉搜索樹像矛盾,以上得證。

上述證實了該問題具備最優子結構性質,接着咱們考慮如何經過子問題的最優解來構造原問題的最優解。通常地,給定關鍵字序列 \(k_i,...,k_j,1 \leqslant i \leqslant j \leqslant n\),其葉結點必然是僞關鍵字\(d_{i-1}...d_j\)。假定關鍵字\(k_r(i \leqslant r \leqslant j)\)是這些關鍵字的最優子樹的根結點,那麼\(k_r\)的左子樹包含關鍵字\(k_1, k_2...k_{r-1}\),其右子樹包含關鍵字\(k_{r+1}...k_j\)。只要咱們檢查全部可能的根結點\(k_r\),並對其左右子樹分別求解,便可保證找出原問題的最優解。

2.2 子問題重疊

從以上分析中咱們發現,求解\(k_r\)的左右子樹問題和求解原問題的模式是同樣的,所以咱們能夠用一個遞歸式來描述原問題的解。

定義\(e(i, j)(其中i\geqslant 1,j \leqslant n 且j \leqslant i-1)\)爲在包含關鍵字\(k_i...k_j\)的最優二叉搜索樹中進行一次搜索的指望代價;\(w(i, j)\)表示以下包含關鍵字\(k_i,...,k_j\)的子樹的全部元素機率之和:

\[ w(i, j) = \sum_{k=i}^j p_k + \sum_{k=i}^j q_k \]

當一棵樹成爲一個結點的子樹時,其上的每個結點的深度都會增長1。根據以前的指望搜索代價\(E(T)\)的計算公式的變形式

\[ E(T) = 1 + \sum_{i=1}^ndepth(k_i) \cdot p_i + \sum_{i=0}^n depth(d_i) \cdot q_i \]

咱們能夠得出,當包含關鍵字\(k_i...k_j\)的最優二叉搜索樹中的每一個結點的深度增長1時,\(E(T)\)將增長\(w(i, j)\)。因而咱們可獲得以下公式:

\[ e(i, j) = p_r + [e(i, r-1) + w(i, r-1)] + [e(r+1, j) + w(r+1, j)] \]

注意到:

\[ w(i, j) = w(i, r-1) + p_r +w(r+1, j) \]

因而上述\(e(i, j)\)遞推式可簡化爲:

\[ e(i, j) = e(i, r-1) + e(r+1, j) + w(i, j) \]

須要注意的是,咱們在上面定義\(e(i, j)\)時,給出了\(i\)\(j\)的取值範圍。其中有種特別的狀況是,當\(j=i-1\)時,表示只包含「僞關鍵字」結點\(d_{r-1}\),此時搜索指望代價\(e(i, i-1) = q_{i-1}\)

根據以上分析,咱們可得最終的遞推式:

\[ e(i, j) = \begin{cases} q_{i-1} & \text{ 若$j = i-1$}\\ \min \limits_{i\leqslant r \leqslant j}[e(i, r-1)+ e(r+1, j) + w(i, j)] & \text { 若$i\leqslant j$} \end{cases} \]

咱們的最終目標是計算出\(e(1, n)\)

矩陣鏈的乘法問題同樣,該問題的子問題是由連續的下標子域構成,許多子問題「共享」另外一些子問題,即子問題重疊

3. 解決問題

有了前兩部分的分析,咱們能夠很容易地設計出一個自底向上的動態規劃算法來解決該問題。

下面給出Python的實現版本:

# 計算e與root

# e[i][j]意思與上述分析中的e(i, j)一致;
# root[i][j]表示包含關鍵字k_i...k_j的最優二叉搜索樹的根結點關鍵字的下標;
# w[i][j]意思也與上述分析中的w(i, j)一致,這裏用w數組做爲「備忘錄」,
# 用公式 w[i][j] = w[i][j - 1] + p[j] + q[j]來計算w[i][j],可利用上以前計算出的w[i][j - 1],
# 這樣能夠節省Θ(j - i)次加法。
def optimal_bst(p, q, n):
    e = [[0 for i in range(n + 1)] for j in range(n + 2)]
    w = [[0 for i in range(n + 1)] for j in range(n + 2)]
    root = [[0 for i in range(n + 1)] for j in range(n + 1)]
    for i in range(1, n + 2):
        e[i][i - 1] = q[i - 1]
        w[i][i - 1] = q[i - 1]
    for l in range(1, n + 1):
        for i in range(1, n - l + 2):
            j = i + l - 1
            e[i][j] = float('inf')
            w[i][j] = w[i][j - 1] + p[j] + q[j]
            for r in range(i, j + 1):
                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 e, root

# 先序遍歷打印樹
def printByPreorderingTraverse(root, i, j):
    if i > j:
        return
    r = root[i][j]
    print(r, end='')
    if r > 0 :
        printByPreorderingTraverse(root, i, r - 1)
    if r < len(root) - 1:
        printByPreorderingTraverse(root, r + 1, j)

if __name__ == '__main__':
    p = [0, 0.15, 0.10, 0.05, 0.10, 0.20] 
    q = [0.05, 0.10, 0.05, 0.05, 0.05, 0.10]
    n = 5
    e, root = optimal_bst(p, q, n)
    print('最優指望搜索代價爲:', e[1][n])
    print('最優搜索二叉樹的先序遍歷結果爲:', end='')
    printByPreorderingTraverse(root, 1, n)

打印結果爲:

最優指望搜索代價爲: 2.75
最優搜索二叉樹的先序遍歷結果爲:21543
相關文章
相關標籤/搜索