1. 程式人生 > >【經典問題】二維動態規劃問題:求最長公共子序列LCS

【經典問題】二維動態規劃問題:求最長公共子序列LCS

原博地址:http://blog.csdn.net/yysdsyl/article/details/4226630

                 http://blog.csdn.net/ljyljyok/article/details/77905681

    證明:   http://blog.csdn.net/waltonhuang/article/details/52032463

最長公共子序列問題是一個經典的電腦科學問題,也是資料比較程式,比如Diff工具,和生物資訊學應用的基礎。它也被廣泛地應用在版本控制,比如Git用來調和檔案之間的改變。

對於任意數量的序列的LCS問題屬於NP-hard,但對於兩個已知長度(m、n)的序列,可以用動態規劃的思想在多項式時間內解決。

假設兩個字串一個為A[n],另一個為B[m],引入二維陣列c[  ][  ],用c[ i ][ j ]記錄A[ i ]與A[ j ]的LCS長度,那麼c[ i ][ j ]的遞推公式為:

if(i==0||j==0)
    c[i][j] = 0;        //邊界初始化
else if(A[i]==B[j])     //子串的最後一個字元相等
    c[i][j] = c[i-1][j-1] + 1;    //LCS的長度必然要比去掉這個相等的字元時的子串的LCS長度大1
else                    //子串的最後一個字元不相等
    c[i][j] = max( c[i-1][j] , c[i][j-1] );    //LCS的長度必然 大於等於 去掉A中最後一個字元或去掉B中最後一個字元時的子串的LCS的長度

這個狀態轉移方程的證明如下:(轉載自http://blog.csdn.net/waltonhuang/article/details/52032463)

  • A[i] == B[j]時 
    顯然,由於兩個字串的最後一位相同,S[i][j]的長度應該比S[i-1][j-1]大1.

  • A[i] != B[j]時 
    結論是:c[i][j] = max(c[i-1][j], c[i][j-1])

    • 證明:

      1. 顯然,由於添加了一個字元,c[i][j] 肯定大於等於c[i-1][j],也肯定大於等於c[i][j-1]
      2. 但是,c[i][j] 不能同時大於 c[i-1][j]c[i][j-1]。 
        反證法: 
        1.1. 如果c[i][j] > c[i-1][j]

        ,說明S[i][j]的最後一位是A[i]。(因為多了A[ i ]這個字元之後,最長公共子序列的長度多了1,所以多出來的這一位就是A[ i ]!) 
        1.2. 同理,如果c[i][j] > c[i][j-1],說明S[i][j]的最後一位是B[j]。 
        1.3. 那麼如果c[i][j] 同時大於 c[i-1][j]c[i][j-1],說明S[i][j]的最後一位既是A[i],也是B[j]。那麼A[i]就與B[j]相同了!矛盾了!所以反證出: c[i][j] 不能同時大於 c[i-1][j]c[i][j-1]。

現在舉例說明,假設string A = "ABCBDAB",string B = "BDCABA",那麼A.size()=7,B.size()=6,引入c[7+1][6+1]=c[8][7],多一個空間是為了存放邊界值,即i=0或j=0時LCS都等於0。

然後排表如下圖所示:(計算順序是從i=1到i=7,以及j=1到j=6的)

可以看到,最右下角的c[7][6] = 4就是要求的LCS的值了。

程式碼如下:

class LCS {
public:
    int findLCS(string A, int n, string B, int m) {
        // write code here
        int c[n+1][m+1];
        int i,j;
        memset(c, 0, sizeof(c));
        for(i=1;i<=n;++i){    // i=0或j=0預設初始化為0
            for(j=1;j<=m;++j){
                if(A[i-1]==B[j-1])
                    c[i][j] = c[i-1][j-1] + 1;
                else
                    c[i][j] = max(c[i-1][j],c[i][j-1]);
            }
        }
        return c[n][m];
    }
};

如果要求輸出這個最長公共子序列,則需要通過陣列c進行回溯:

    string lcstr;
    i = n;    //將i、j下標落到陣列c的末尾元素上。
    j = m;
    while (i != 0 && j != 0)
    {
        if (A[i] == B[j]) {   //將相同的子元素壓棧。然後指標前移,直到i、j指向0終止(因為任何字串 與0 求公共子序列,都是0)
            lcstr.push_back(A[i]);
            --i;
            --j;
        }
        else { //若二者不相等,而最長公共子序列一定是由LCS(c[i][j-1] or c[i-1][j])的較大者得來,故將較大者的指標前移,接著遍歷。
            if (c[i][j - 1] > c[i - 1][j]) {
                --j;        //將當前列前移到j-1列
            }
            else { // if(c[i][j - 1] <= c[i - 1][j])
                --i;
            }
        }
    }
    //求LCS(之一)
    reverse(lcstr.begin(), lcstr.end());