動態規劃之LCS

最長公共子序列的意思就是兩個序列,有公共的部分,公共部分在這兩個序列的位置上不一定相等,但序列的邏輯順序是相等的

例如給定兩個序列x[1..m]y[1..n],找出一個(注,這裏說的是一個而不是這個,也就是說可能有很多個)最長的公共序列,其中

 x: A B C B D A B          y: B D C A B A

LCS(x,y) = BCBA   .........此處LCS(x,y)是一種函數標識形式,但不是函數

枚舉算法:

x[1..m]中每一個子序列判斷是否也是y[1..n]中的一個子序列

這個算法對於x的每一個子序列y需要On)來比較,而x2m個子序列(注,可以當成一個長度爲mbit-vector即位向量來找出不同的子序列)

這個算法的最壞運行時間是On2m)

遞歸算法:

策略是考慮xy的前綴

定義:c[i,j] = | LCS(x[1..i],y[1..j])| c[m,n] = | LCS(x,y)|

上面c[i,j]表示x[1..i]y[1..j]的最長公共序列的長度,c[m,n]表示x[1..m]y[1..n]的最長公共序列的長度

則遞歸公式:

證明:在x[i] = y[j]情況下,如下圖:

設z[1..k] = LCS(x[1..i],y[1..j])x,y的一個最長公共序列是zc[i,j] = k那麼z[k] = x[i] = y[j]

顯然z[1..k-1]x[1..i-1]y[1..j-1]的一個公共序列則令z[1..k-1] = LCS(x[1..i-1],y[1..j])爲了推出矛盾,假設w是一個比c[i,j]-1更長的關於 x[1..i-1]y[1..j-1]的一個公共序列,即|w| > k-1

然後利用cut and paste(剪切粘貼)方法 w||z[k]w concatenated with z[k])得到 x[1..i]y[1..j]的一個公共子序列,然而|w||z[k]| >k,推出矛盾,所以c[i-1,j-1] = k-1意味着c[i,j] = c[i-1,j-1] +1

另一個方面即x[i] 與 y[j] 不相等,證明過程相同,此處省略。

這樣就得到了Dynamic-programming的一個特徵:

1一個問題的最優解包含了子問題的最優解

即如果z = LCS(x,y)則任何z的前綴必是x的前綴和y的前綴的一個最長公共子序列

代碼如下:

下面有一個遞歸樹來說明問題:

這個樹的高度是m+n,觀察這個樹我們發現重複計算了很多子問題這樣就引出了Dynamic-programming的另一個特徵:

2一個遞歸的過程包含少數獨立問題被反覆計算多次

求上面給出的兩個長度分別爲m,n的字符串的LCS的子問題數爲mn

不是嚴格的分析:以上圖爲例樹中節點包含了所有情況且有重複,m=3有一個集合{123}n = 4有一個集合{1234},則從集合m和集合n中各取一個元素即{x,y/x∈m或者x∈ny∈m或者y∈n},我們把重複的節點去掉就變成了{x,y/x∈my∈n}即子問題個數爲mn

爲了避免重複計算我們把每一個子問題的結果都存到一張表裏,當有一個新的子問題的時候如果前面已經計算過了就直接到表裏取就可以了

代碼如下:

時間複雜度和空間複雜度都爲Omn

爲了更清楚的理解這個問題,以開始舉的例子爲例用這種方法來計算LCS

圖標中的第一行和第一列是LCS的初始值全爲0y中第一個子序列即B開始在x的所有子序列中如果有B就置1這樣就完成了第二行,第三行即從y中一個子序列BD開始在x所有子序列中如果有D就在上一行對應的位置下面加一表示LCS的長度,以此類推可以完成整個表格的填寫。

完成表格以後就要找到一條路徑即爲LCS這條路徑的找法分爲兩種:

1從上往下:從第一行開始找到一條路徑使這個方格的後續值是相同的且隨行數遞增

2從下往上:從最後一行開始通過行減1或者列減1得到一個依次遞減的序列

利用上面兩種方法都可以找到LCS

Python實現的代碼如下:

def LCS(x,y):
    d = [ [ None for i in x ] for j in y ] 
    m = [ [ 0 for k in x] for v in y]
    for p1 in range(len(y)): 
        for p2 in range(len(x)): 
            if y[p1] == x[p2]:
                if p1 == 0 or p2 == 0: 
                    m[p1][p2] = 1 
                else: 
                    m[p1][p2] = m[p1-1][p2-1]+1 
                d[p1][p2] = 1
            elif m[p1-1][p2] < m[p1][p2-1]: 
                m[p1][p2] = m[p1][p2-1] 
                d[p1][p2] = 2
            else:                             # m[p1][p2-1] < m[p1-1][p2] 
                m[p1][p2] = m[p1-1][p2] 
                d[p1][p2] = 3
    
    s = []
    (p1, p2) = (len(y)-1, len(x)-1) 
    while p1 >=0 and p2>=0 and m[p1][p2]>0:
        print(p1,p2)
        c = d[p1][p2]
        if c == 1: 
            s.append(y[p1])
            p1 -=1
            p2 -=1 
        if c == 2: p2 -= 1 
        if c == 3: p1 -= 1 
    s.reverse() 
    return s 


x = ['A','B','C','B','D','A','B']
y = ['B','D','C','A','B','A']
print(LCS(x,y))

轉自http://zsp.iteye.com/blog/379632

關於代碼實現我看了好長時間纔看懂了過程,不是別的原因主要是自己太菜了,爲了防止以後忘記我將過程詳細表述如下:

M用來存放lcs長度,d用來存放回溯“指針”,他們大小都是xy,下面用y中每一個元素和x中的所有元素進行比較,如果兩個元素相等且其中一個元素是所在序列的第一個則m[p1][p2] = 1 ,有前面講的前綴法,則m[p1][p2] = m[p1-1][p2-1]+1 

在兩個元素相等的情況下令d[p1][p2] = 1表示這個元素屬於公共序列,然後當兩個元素不相等又分爲m[p1-1][p2] < m[p1][p2-1]m[p1-1][p2]>= m[p1][p2-1],第一種情況列減1大於行減1m[p1][p2] = m[p1][p2-1],且讓d[p1][p2] = 2,其實d的數值可以任意設定,只不過要注意在輸出lcs時在列減1處大的d設定對應的回溯是列減1,第二種情況就和第一種情況相反,感覺沒有說清楚,好吧,畫一張表格對應着看:

其中黃色表示d中數值,黑色表示m中數值,紅色塊表示回溯過程(自底向上)


 

發佈了82 篇原創文章 · 獲贊 3 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章