動態規劃之LCS演算法
一、前言
LCS是Longest Common Subsequence的縮寫,即最長公共子序列。一個序列,如果是兩個或多個已知序列的子序列,且是所有子序列中最長的,則為最長公共子序列。
另外還有個分支問題:最長公共子串。子串的字元位置必須連續,而子序列則不必,從原序列中去掉任意的元素獲得的新序列。可以看出,子串問題比子序列問題要簡單地多,子串必定是子序列,換言之,子串是子序列的子集。如果我們能解決子序列問題,子串問題也迎刃而解。
二、解法
2.1窮舉法
窮舉法是顯而易見第一時間從腦子裡蹦出來的想法,實際上程式碼層面的實現也不困難。提取出A序列的每一個子序列,檢查其是否也是B序列的子序列,全部比對完後,比較出最長的一個子序列。
不考慮子序列重複的前提下啊,一個長度為n的序列,其子序列個數為2^n(容易理解,每一項取或不取)。易知其時間複雜度為O(2^n),指數級複雜度一般來說是不可接受的。
這裡的空間複雜度我看一些文章說也是O(2^n),但是我覺得並不需要存下每一個子序列,每一個A的子序列經驗證不是B的子序列後即可丟棄,所以儲存的花費並不是所有子序列,而是所有公共子序列。所以我認為空間複雜度沒有達到O(2^n),可能是我的理解有問題,如果有懂得觀眾看到這裡,懇請指點一二。
2.2動態規劃
記X = [x1,x2,...,xm]
和Y = [y1,y2,...,yn]
的一個最長公共子序列Z = [z1,z2,...,zk]
,則有:
1. 若xm=yn,則zk=xm=yn且Zk-1是Xm-1和Yn-1的最長公共子序列;
2. 若xm≠yn且zk≠xm,則Z是Xm-1和Y的最長公共子序列;
3. 若xm≠yn且zk≠yn,則Z是X和Yn-1的最長公共子序列。
其中Xm-1 = [x1, x2, …, xm-1]
,Yn-1 = [y1, y2, …, yn-1]
,Zk-1 = [z1, z2, …, zk-1]
。
第2點和第3點可以合併為,max(LCS(Xm-1,Yn),LCS(Xm,Yn-1))
2.3矩陣思想解題
記一個二維陣列C[]
,c[i,j]儲存Xi和Yi的最長公共子序列的長度。所以c[m,n]即矩陣最右下角的值為X與Y的最長公共子序列的長度。
雖然我們在遞推過程是從序列的尾部開始的,但實際解題是從頭部開始的,因為在計算max(LCS(Xm-1,Yn),LCS(Xm,Yn-1))
時,需要事先計算出LCS(Xm-1,Yn)
和LCS(Xm,Yn-1)
,才能比較他們的大小。
1. 先令c[i,0]整一列的值為0,顯然任意序列與空序列的最長公共子序列長度為0;同理,令c[0,j]整一行的值為0;
2. 如果當前比較的兩個字元xi=yj
,令這個格子的c[i,j] = 1
。方向為左上角(LeftTop);
3. 如果當前比較的兩個字元xi≠yj
4. 一直迭代運算至二維陣列C[]所有格子均有值,結束。
便於理解抄自網路的圖:
2.4小結
記錄方向是為了構造出最長公共子序列,當然這樣的演算法有一個侷限就是當LCS(Xm-1,Yn) = LCS(Xm,Yn-1)
時會出現多解,即最長公共子序列不唯一。這樣的情況顯然是可預見的,所以在當出現LCS(Xm-1,Yn) = LCS(Xm,Yn-1)
時兩個方向都得記錄,才能恢復出所有的最長公共子序列(如果有需要)。
當然,如果只是為了求得最長公共子序列的長度,方向是不必記錄的。連矩陣都可以不用構造,因為c[i,j]的值完全來源於上一行的值,即c[i-1,j-1]、c[i-1,j]、c[i,j-1]三者其中之一,只需要記錄矩陣中的兩行資料即可,空間複雜度進一步降低。
2.5子問題1——最長公共子串
解決了最長公共子序列問題,最長公共子串就簡單地多了。仍然是構造二維矩陣C[]
,當xi = yj
時,令c[i,j] = c[i-1,j-1]
,然後矩陣中最大的元素就是最長公共子串的長度。構造最長公共子串也只需要找出最長的一條斜對角線即可。
附Python實現:
def find_lcs_len(input_x, input_y):
dp = [([0] * len(input_y)) for i in range(len(input_x))]
maxlen = 0
for i in range(0, len(input_x)):
for j in range(0, len(input_y)):
if input_x[i] == input_y[j]:
if i != 0 and j != 0:
dp[i][j] = dp[i - 1][j - 1] + 1
if i == 0 or j == 0:
dp[i][j] = 1
if dp[i][j] > maxlen:
maxlen = dp[i][j]
return maxlen
2.6子問題2——最長遞增子序列(LIS)
看到這有些人可能會疑惑,最長遞增子序列只關係到一個序列。如序列X = [5,8,2,3,9,4,7]
的LIS為[2,3,4,7]
。而LCS問題是兩個序列的公共子序列問題。
其實這裡先構造一個輔助序列X' = [2,3,4,5,7,8,9]
,即對X排序生成的新序列。對序列X和X’求LCS就是這個問題的解。這裡不再詳細論述,相信聰明的讀者都容易看懂其中邏輯。
三、總結
用LCS演算法代替窮舉法來解決最長公共子序列問題,時間複雜度由O(2^n)下降到了O(n*m),空間複雜度也是同等級數的下降。經由精妙的LCS演算法,為我們方便地解決了運算起來繁複的問題。
有機會得繼續學習這些有趣奇妙的演算法。另外,我也得花時間去理解下複雜度的計算,之前一直是我的盲點。
收!