最長公共子序列(LCS)


      最長公共子序列,英文縮寫為LCS(Longest Common Subsequence)。其定義是,一個序列 S ,如果分別是兩個或多個已知序列的子序列,且是所有符合此條件序列中最長的,則 S 稱為已知序列的最長公共子序列。而最長公共子串(要求連續)和最長公共子序列是不同的.

      最長公共子序列是一個十分實用的問題,它可以描述兩段文字之間的"相似度",即它們的雷同程度,從而能夠用來辨別抄襲。對一段文字進行修改之后,計算改動前后文字的最長公共子序列,將除此子序列外的部分提取出來,這種方法判斷修改的部分,往往十分准確。  

動態規划法

     經常會遇到復雜問題不能簡單地分解成幾個子問題,而會分解出一系列的子問題。簡單地采用把大問題分解成子問題,並綜合子問題的解導出大問題的解的方法,問題求解耗時會按問題規模呈冪級數增加。為了節約重復求相同子問題的時間,引入一個數組,不管它們是否對最終解有用,把所有子問題的解存於該數組中,這就是動態規划法所采用的基本方法。

      算法

動態規划的一個計算兩個序列的最長公共子序列的方法如下:

以兩個序列 X、Y 為例子:

設有二維數組f[i,j] 表示 X 的 i 位和 Y 的 j 位之前的最長公共子序列的長度,則有:

f[1][1] = same(1,1);

f[i,j] = max{f[i-1][j -1] + same(i,j),f[i-1,j],f[i,j-1]}

其中,same(a,b)當 X 的第 a 位與 Y 的第 b 位相同時為"1",否則為"0"。

此時,二維數組中最大的數便是 X 和 Y 的最長公共子序列的長度,依據該數組回溯,便可找出最長公共子序列。

該算法的空間、時間復雜度均為O(n^2),經過優化后,空間復雜度可為O(n)。

 

     【問題】 求兩字符序列的最長公共字符子序列

問題描述:字符序列的子序列是指從給定字符序列中隨意地(不一定連續)去掉若干個字符(可能一個也不去掉)后所形成的字符序列。令給定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一個嚴格遞增下標序列<i0,i1,…,ik-1>,使得對所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一個子序列。

考慮最長公共子序列問題如何分解成子問題,設A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,並Z=“z0,z1,…,zk-1”為它們的最長公共子序列。不難證明有以下性質:

(1) 如果am-1=bn-1,則zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一個最長公共子序列;

(2) 如果am-1!=bn-1,則若zk-1!=am-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列;

(3) 如果am-1!=bn-1,則若zk-1!=bn-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列。

這樣,在找A和B的公共子序列時,如有am-1=bn-1,則進一步解決一個子問題,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。

      求解:

      引進一個二維數組c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜索的方向。
我們是自底向上進行遞推計算,那么在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] = Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。

回溯輸出最長公共子序列過程:

    我來說明下此圖(參考算法導論)。在序列X={A,B,C,B,D,A,B}和 Y={B,D,C,A,B,A}上,由LCS_LENGTH計算出的表c和b。第i行和第j列中的方塊包含了c[i,j]的值以及指向b[i,j]的箭頭。在c[7,6]的項4,表的右下角為X和Y的一個LCS<B,C,B,A>的長度。對於i,j>0,項c[i,j]僅依賴於是否有xi=yi,及項c[i-1,j]和c[i,j-1]的值,這幾個項都在c[i,j]之前計算。為了重構一個LCS的元素,從右下角開始跟蹤b[i,j]的箭頭即可,這條路徑標示為陰影,這條路徑上的每一個“↖”對應於一個使xi=yi為一個LCS的成員的項(高亮標示)。

      所以根據上述圖所示的結果,程序將最終輸出:“B C B A”。

算法分析:
由於每次調用至少向上或向左(或向上向左同時)移動一步,故最多調用(m + n)次就會遇到i = 0或j = 0的情況,此時開始返回。返回時與遞歸調用時方向相反,步數相同,故算法時間復雜度為Θ(m + n)。

#include <stdio.h>
#include <string.h>
#define MAXLEN 100

void LCSLength(char *x, char *y, int m, int n, int c[][MAXLEN], int b[][MAXLEN])
{
    int i, j;
    for (i = 0; i <= m; i++)
        c[i][0] = 0;
    for (j = 1; j <= n; j++)
        c[0][j] = 0;
    for (i = 1; i <= m; i++)
    {
        for (j = 1; j <= n; j++)
        {
            if (x[i - 1] == y[j - 1])
            {
                c[i][j] = c[i - 1][j - 1] + 1;
                b[i][j] = 0;
            }
            else if (c[i - 1][j] >= c[i][j - 1])
            {
                c[i][j] = c[i - 1][j];
                b[i][j] = 1;
            }
            else
            {
                c[i][j] = c[i][j - 1];
                b[i][j] = -1;
            }
        }
    }
}

void PrintLCS(int b[][MAXLEN], char *x, int i, int j)
{
    if (i == 0 || j == 0)
        return;
    if (b[i][j] == 0)
    {
        PrintLCS(b, x, i - 1, j - 1);
        printf("%c ", x[i - 1]);
    }
    else if (b[i][j] == 1)
        PrintLCS(b, x, i - 1, j);
    else
        PrintLCS(b, x, i, j - 1);
}

int main(int argc, char **argv)
{
    char x[MAXLEN] = { "ABCBDAB" };
    char y[MAXLEN] = { "BDCABA" };
    int b[MAXLEN][MAXLEN];
    int c[MAXLEN][MAXLEN];
    int m, n;

    m = strlen(x);
    n = strlen(y);

    LCSLength(x, y, m, n, c, b);
    PrintLCS(b, x, m, n);

    return 0;
}

分兩個部分:(參考:http://blog.csdn.net/v_july_v/article/details/6695482)

1)計算最優值  LCSLength()

  計算最長公共子序列長度的動態規划算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作為輸入。輸出兩個數組c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存儲Xi與Yj的最長公共子序列的長度,b[i,j]記錄指示c[i,j]的值是由哪一個子問題的解達到的,這在構造最長公共子序列時要用到。最后,X和Y的最長公共子序列的長度記錄於c[m,n]中。

   由算法LCS_LENGTH計算得到的數組b可用於快速構造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最長公共子序列。首先從b[m,n]開始,沿着其中的箭頭所指的方向在數組b中搜索。

  • 當b[i,j]中遇到"↖"時(意味着xi=yi是LCS的一個元素),表示Xi與Yj的最長公共子序列是由Xi-1與Yj-1的最長公共子序列在尾部加上xi得到的子序列;
  • 當b[i,j]中遇到"↑"時,表示Xi與Yj的最長公共子序列和Xi-1與Yj的最長公共子序列相同;
  • 當b[i,j]中遇到"←"時,表示Xi與Yj的最長公共子序列和Xi與Yj-1的最長公共子序列相同。

    這種方法是按照反序來找LCS的每一個元素的。由於每個數組單元的計算耗費Ο(1)時間,算法LCS_LENGTH耗時Ο(mn)。

2)構造最長公共子序列  PrintLCS()

     在算法LCS中,每一次的遞歸調用使i或j減1,因此算法的計算時間為O(m+n)。

 

算法的改進:參考:http://blog.csdn.net/v_july_v/article/details/6695482

變形題型:參考:http://www.cnblogs.com/zhangchaoyang/articles/2012070.html(最大子序列、最長遞增子序列、最長公共子串、最長公共子序列、字符串編輯距離

/**
 * 第一步:論證是否是動態規划問題
 * 首先要證明最長公共子序列問題是動態規划問題,即符合動態規划算法的兩個特點:最優子結構和重疊子問題
 * 最優子結構:
 * 記:Xi=﹤x1...xi﹥即X序列的前i個字符 (1≤i≤m)(前綴)        Yj=﹤y1...yj﹥即Y序列的前j個字符 (1≤j≤n)(前綴)
 * 假定Z=﹤z1...zk﹥∈LCS(X , Y)。
 * 若xm=yn(最后一個字符相同),則問題化歸成求Xm-1與Yn-1的LCS(LCS(X , Y)的長度等於LCS(Xm-1 , Yn-1)的長度加1)。
 * 若xm≠yn,則問題化歸成求Xm-1與Y的LCS及X與Yn-1的LCS。LCS(X , Y)的長度為:max{LCS(Xm-1 , Y)的長度, LCS(X , Yn-1)的長度}。
 * 由於上述當xm≠yn的情況中,求LCS(Xm-1 , Y)的長度與LCS(X , Yn-1)的長度,這兩個問題不是相互獨立的:兩者都需要求LCS(Xm-1,Yn-1)的長度。
 * 另外兩個序列的LCS中包含了兩個序列的前綴的LCS,故問題具有最優子結構性質。
 * 重疊子問題:
 * 在計算X和Y的最長公共子序列時,可能要計算出X和Yn-1及Xm-1和Y的最長公共子序列,
 * 而這兩個子問題都包含一個公共子問題,即計算Xm-1和Yn-1的最長公共子序列,因此最長公共子序列問題具有子問題重疊性質。
 *
 * 第二步:建立遞歸式
 * 用c[i][j]記錄序列Xi和Yj的最長公共子序列的長度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。建立遞歸關系如下:
 *            0                 if i=0||j=0
 * c[i][j]=   c[i-1][j-1]+1     if i,j>0&&x[i]==y[j]
 *            max(c[i][j-1],c[i-1][j]) if i,j>0&&x[i]!=y[j];
 */
public class LCS {


    /**第三步:計算最優值
     * 計算最長公共子序列長度的動態規划算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作為輸入。
     * 輸出兩個數組c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存儲Xi與Yj的最長公共子序列的長度,b[i,j]記錄指示c[i,j]的值是由哪一個子問題的解達到的,
     * 這在構造最長公共子序列時要用到。最后,X和Y的最長公共子序列的長度記錄於c[m,n]中。
     * 在這里可以將數組b省去。事實上,數組元素c[i,j]的值僅由c[i-1,j-1],c[i-1,j]和c[i,j-1]三個值之一確定,而數組元素b[i,j]也只是用來指示c[i,j]究竟由哪個值確定。
     * 因此,在算法LCS中,我們可以不借助於數組b而借助於數組c本身臨時判斷c[i,j]的值是由c[i-1,j-1],c[i-1,j]和c[i,j-1]中哪一個數值元素所確定,代價是Ο(1)時間。
     * @param x
     * @param y
     * @return
     */
    public int[][] lcsLength(char x[],char y[]){
        int m = x.length;
        int n = y.length;
        int [][]c = new int[m+1][n+1];
        for(int i = 0;i<m+1;i++)
            c[i][0]=0;
        for(int j = 0;j<n+1;j++)
            c[0][j]=0;
        for(int i = 1;i<=m;i++){
            for(int j = 1;j<=n;j++){
                //i,j從1開始,所以下面用i-1和j-1使得可以從數組0元素開始
                if(x[i-1]==y[j-1]){
                    c[i][j] = c[i-1][j-1]+1;
                }else if(c[i-1][j]>=c[i][j-1]){
                    c[i][j]=c[i-1][j];
                }else{
                    c[i][j]=c[i][j-1];
                }
            }
        }
        return c;
    }
    /**第四步:構造最長公共子序列
     * lcs函數用來構造最長公共子序列,它使用在計算最優值中得到的c數組可以快速的
     * 構造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最長公共子序列。
     * @param c
     * @param x
     * @param i
     * @param j
     */
    public void lcs(int c[][],char x[],int i,int j){
        if(i==0||j==0)return;
        if(c[i][j]==(c[i-1][j-1]+1)){
            lcs(c,x,i-1,j-1);
            //注意c的長度要比x大1
            System.out.println(x[i-1]);
        }else if(c[i][j]==c[i-1][j]){
            lcs(c,x,i-1,j);
        }else{
            lcs(c,x,i,j-1);
        }
    }
    //測試
    public static void main(String args[]){
        char x[]={'A','B','C','B','D','A','B'};
        char y[]={'B','D','C','A','B','A'};
        LCS lcs = new LCS();
        int [][]c = lcs.lcsLength(x, y);
        lcs.lcs(c, x, x.length, y.length);
    }
}

 

參考:

程序員面試100題之六:最長公共子序列

LCS算法

http://www.cnblogs.com/zhangchaoyang/articles/2012070.html

http://blog.csdn.net/yysdsyl/article/details/4226630


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM