演算法導論
這個文件是學習“演算法設計與分析”課程時做的筆記,文件中包含的內容包括課堂上的一些比較重要的知識、例題以及課後作業的題解。主要的參考資料是 Introduction to algorithms-3rd(Thomas H.)(對應的中文版《演算法導論第三版》),除了這本書,還有的參考資料就是 Algorithms design techniques and analysis (M.H. Alsuwaiyel)。
動態規劃法
動態規劃(Dynamic Programing)和分治法一樣,透過合併子問題的解決結果來解決當前的問題。
前面已經介紹過,分治法是透過將問題拆分成若干個不相交的子問題,然後遞迴解決這些子問題,最後再將子問題的解決結果合併成原問題的解決結果。
與之相比,動態規劃則是當子問題存在重疊部分的時候使用的,也就是說當若干個子問題(subproblems)都有一個共同的子子問題(sub-subproblems)的時候,分治法就沒必要再使用了,因為這種情況下分治法會重複地解決同一個子子問題,所以這個時候就需要動態規劃來解決。
需要注意的是,無論是分治還是動態規劃,其子問題都必須是可以獨立求解的,也就是說後面的子問題的解不會依賴前面的子問題的解。
動態規劃對同一個子子問題只會解決一次,並且會將解決結果存放在一個表中,從而避免重複計算同一個子子問題。
動態規劃通常用於最優解(更確切地說是全域性最優解)問題,也就是說當前的問題可以有很多中解決方案,而每一個解決方案都有一定的值(或者說是收益),而我們希望能找到收益最大化的解決方案,並且將這樣的解決方案稱為最優解。
動態規劃一般分為下面四個步驟:
- Characterize: 刻畫一個最優解的結構特徵
- Define: 遞迴定義最優解的值
- Compute: 計算最優解的值,通常採用自底向上的方法
- Construct: 利用計算出的資訊構造一個最優解
這裡介紹一個切割鋼條(Rod cutting)的例子:假設現在有一個長度為n的鋼條,現在要對其進行切割,鋼條的價格與鋼條的長度並不是線性相關的,比如:
Length | Price |
---|---|
1 | 1 |
2 | 5 |
3 | 8 |
4 | 9 |
5 | 10 |
現在問如何切割這跟鋼條?使其能夠買到最高的價格。
首先是要刻畫最優解的結構特徵,並遞迴定義最優解的值。
那麼現在有一個問題就是:第一次切割應該如何切割才能讓其收益最大化?
假設第一次切割後將鋼條分為長度分別為 i 和 n-i 的兩段鋼條,那麼這個時候鋼條的收益可以記為:
\(S(n) = p(i) + r(n-i)\)
其中 S(n) 代表長度為 n 的鋼條的此時的收益,p(i) 代表長度為 i 的鋼條的直接售價,r(n-i) 代表長度為 n-i 的鋼條的最大收益。
那麼顯然,長度為 n 的鋼條的最大收益應該表示為:
\(r(n) = \max_{i = 1, \dots n}(p(i)+ r(n-i))\)
接下來的步驟就和分治思想比較相似了。
但是有一個需要注意的點是,在這個問題中,每個子問題之間是存在重疊的。比如在 r(n-1) 這個子問題中,包含的子問題有 r(n-2), r(n-3), ...,而在同一個遞迴層次的另一個子問題 r(n-2) 中,包含的子問題有 r(n-3), r(n-4), ...。
因為子問題之間的重疊性,所以在動態規劃中通常採用自底向上的遞迴思路,並且還需要採用一個“備忘錄”來避免重複計算同一個子子問題。例如在上面的這個例子中,可以使用一個長度為 n 的全域性陣列來儲存每個子問題的解 即 r(i) 。
最大公共子序列
這裡將要介紹另一個例子,並且將透過這個例子介紹最優子結構性質與子問題重疊性質,這兩個性質是動態規劃中的兩個關鍵的概念,通常能夠使用動態規劃解決的問題都具備這兩種性質。
最大公共子序列問題,給定兩個字串 A 和 B,長度分別為 n 和 m,字母表記為 \(\Sigma\),確定兩個字串的最大公共子序列,這裡的最大公共子序列的定義是,只考慮子序列的順序而不需要考慮子序列中的字元是否是相鄰的。
比如給定兩個序列,A = zxyxyz,B = xyyzx,那麼他們的最大公共子序列就是 xyyz。
最簡單的方法是使用暴力破解法:列舉字串 A 的所有子序列,一共有 \(2^n\) 個子序列。然後在字串 B 中逐個搜尋這些子序列,搜尋每個子序列需要進行 m 次比較,能夠搜尋到的最大的子序列就是最大公共子序列, 那麼這種方法的時間複雜度就是 \(O(m 2^n)\) ,這是個指數增長的時間複雜度。
動態規劃方法是基於遞迴的方法,因此,用使用動態規劃的方法解決這個問題就需要找到解決這個問題的遞迴式。
令 \(L[i,j]\) 表示序列 \(a_1,\dots, a_i\) 和序列 \(b_1, \dots, b_j\) 的最大公共子序列的長度。當這兩個序列的其中或者兩個都是空序列時,他們的最大公共子序列的長度為0,也就是說當 \(i = 0\) 或者 \(j = 0\) 時, \(L[i,j] = 0\)。
那麼可以透過觀察得出下面的結論:
- 若 \(a_i = b_j\),則\(L[i,j] = L[i - 1, j- j] + 1\)
- 若 \(a_i\ne b_j\),則\(L[i,j] = \max\{ L[i, j-1], L[i-1, j] \}\)
根據這樣的規律,可以得出最大公共子序列的遞迴式:
遞迴式的思想就是,如果要得到兩個序列的最大公共子序列,那麼可以先計算它們的字首的最大子序列,然後再比較這兩個字首後面的一個字元是否相同再來判斷當前序列的最大公共子序列。
可以使用一個 \((n+1)\times(m+1)\) 的矩陣來儲存 \(L(i,j)\) ,其中 \(0\le i \le n, 0\le j \le m\) 。
演算法的虛擬碼如下:
可以看到這個演算法的時間複雜度為 \(\Theta(nm)\).
最優子結構性質
如果一個問題是具有最優子結構性質的,那麼就可以使用動態規劃進行解決。或者說一個問題必須具備最優子結構性質才能夠使用動態規劃的方法求解。
下面演示如何證明最長公共子序列問題具有最優子結構性質。
最優子結構性質可以理解為,如果一個問題的解的結構可以拆分成子問題的解,那麼子問題的解一定是這個子問題的最優解。
最優子結構性質的證明方法通常是採用反證法進行證明。
設 \(X=(x_1, ..., x_n), Y=(y_1, ..., y_m)\) 是兩個序列,它們的最長公共子序列為 \(Z=(z_1, ..., z_k)\),那麼:
I. 若 \(x_n = y_m\) :
則 \(z_k = x_n = y_m\),那麼顯然有:
\((z_1, ..., z_{k-1})\) 是 \((x_1, ..., x_{n-1})\) 和 \((y_1, ..., y_{m-1})\) 的最長公共子序列。
即\(LCS(n,m) = LCS(n-1, m-1) + 1\)
II. 若 \(x_n\ne y_m\) :
那麼可以分兩種種情況討論:
i. 若 \(z_k \ne x_n\),那麼可以證明 \(Z\) 是 \((x_1, ..., x_{n-1})\) 和 \((y_1, ..., y_n)\) 的最長公共子序列。
反證法,假設 \((x_1, ..., x_{n-1})\) 和 \((y_1, ..., y_m)\) 的最長公共子序列為 \(Z' = (z_1', ..., z_h')\ne Z\),那麼因為 \(x_n\ne y_m, x_n\ne z_k\) 所以\(x_n\) 不會影響 \(X\) 和 \(Y\) 的最長公共子序列,因此,\(Z'\) 也是\((x_1, ..., x_n)\) 與 \((y_1, ..., y_m)\) 的最長公共子序列,而這與前提條件 \(Z\) 是 \((x_1, ..., x_n)\) 與 \((y_1, ..., y_m)\) 的最長公共子序列相矛盾,因此 \(Z\) 必然是 \((x_1,...,x_{n-1})\) 和 \((y_1, ..., y_m)\) 的最長公共子序列。
即 \(LCS(n,m) = LCS(n-1, m)\)
ii. 若 \(z_k\ne y_m\) ,那麼同理可證 \(Z\) 是 \((x_1, ..., x_n)\) 和 \((y_1, ..., y_{m-1})\) 的最長公共子序列。
即 \(LCS(n,m) = LCS(n, m-1)\)
最終,若 \(x_n\ne y_m\),則 \(LCS(n,m) = \max\{ LCS(n-1, m), LCS(n, m-1)\}\)
綜上,最長公共子序列問題具有最優子結構性質,子問題的遞迴結構如下:
子問題的重疊性
分治思想與動態規劃思想都是具有最優子結構性質的,但是動態規劃與分治的一個區別在於,動態規劃解決的問題是具有子問題重疊性質的,而分治解決的問題的子問題則是不重疊的。
最長公共子序列問題的子問題重疊性如下圖所示:
可以看到組成最優解的不同子問題的解是有一定重合的,基於這樣的性質,可以減少計算次數,將其中一個子問題的解記錄下來,到另一個子問題需要用到這個解是就不需要再重新計算。