1. 程式人生 > >動態規劃之LCS演算法

動態規劃之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

,比較c[i-1,j]和c[i,j-1]的值,取其中較大的值填充入c[i,j]中,方向為值的來源方向左(Left)或者上(Top);
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演算法,為我們方便地解決了運算起來繁複的問題。
有機會得繼續學習這些有趣奇妙的演算法。另外,我也得花時間去理解下複雜度的計算,之前一直是我的盲點。
收!