動態規劃之最長公共子序列

這個世界上根本就不存在“不會做”這回事,當你失去了所有的依靠的時候,自然就什麼都會了。

0. 前言

  最長公共子序列的問題常用於解決字符串的相似度,是一個非常實用的算法,作爲碼農,此算法是我們的必備基本功。最長公共子串(Longest Common Substirng)和最長公共子序列(Longest Common Subsequence,LCS)的區別爲:子串是串的一個連續的部分,子序列則是從不改變序列的順序,而從序列中去掉任意的元素而獲得新的序列;也就是說,子串中字符的位置必須是連續的,子序列則可以不必連續。

1.最長公共子序列概述

  問題描述: 字符序列的子序列是指從給定字符序列中隨意地(不一定連續)去掉若干個字符(可能一個也不去掉)後所形成的字符序列。令給定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一個嚴格遞增下標序列

2. 動態規劃求解最長公共子序列  

2.1 刻畫最長公共子序列的最優子結構的特徵

  如果使用暴力搜索求解LCS問題,需要窮舉X的所有子序列,對每一個子序列檢查它是否是Y的子序列,記錄找到的最長的子序列。X的每個子序列對應的X的下標集合是(1,2,3,.....m) 的一個子集,所以有2m 個子序列。但是最長公共子序列有最優子結構的性質。子問題的自然分類對應兩個輸入序列的“前綴”對。
  定理: 考慮最長公共子序列問題如何分解成子問題,設A=“a1,…,am”,B=“b1,…,bn”,並Z=“z1,…,zk”爲它們的最長公共子序列。不難證明有以下性質:
(1) 如果am=bn,則zk=am=bn,且“z1,…,zk-1”是“a1,…,am-1”和“b1,…,bn-1”的一個最長公共子序列;
(2) 如果am!=bn,則若zk!=am,蘊涵“z1,…,zk”是“a1,…,am-1”和“b1,…,bn”的一個最長公共子序列;
(3) 如果am!=bn,則若zk!=bn,蘊涵“z1,…,zk”是“a1,…,am”和“b1,…,bn-1”的一個最長公共子序列。
  上面的定理告訴我們:兩個序列的LCS包含兩個序列前綴的LCS,因此LCS問題具有最優子結構的性質。

2.2 構建遞歸解

  通過上面的定理我們可以發現在求A=a1amB=b1bn 的一個公共的LCS的時候我們需要求解一個到兩個子問題。、
  【1】如果am=bn,我們應該求Am1Bn1 的LCS;
  【2】如果am!=bn,若zk!=am,我們應該求Am1Bn 的LCS。如果am!=bn,則若zk!=bn,我們應該求AmBn1 的LCS。兩者LCS比較長的稱爲A和B的LCS。
  我們可以很容易的發現子問題重疊的性質。在求A和B的LCS的過程中,我們的2種情況,都存在Am1Bn1 的LCS的重疊子問題。設計LCS的遞歸算法首先需要建立最優解的遞歸式,C[i,j]表示Ai和Bj的LCS長度,如果i=j=0,那麼c[i,j]=0.根據LCS問題最優子結構的性質得出以下公式:
  

c[i,j]=  0  c[i1,j1]+1  max(c[i,j1],c[i1,j])i=0j=0i,j>0 Ai = Bji,j>0 Ai!=Bj  

   在上面的問題中我們通過限制條件限定了需要求解哪些子問題。在前面的算法中我們沒有限定排除任何子問題,在這裏我們需要根據限制條件排除相應的子問題。

2.3 計算LCS的長度(求最優解)

根據2.2的遞歸公式,我們可以很痛以的寫出一個指數時間的遞歸算法。但是,由於LCS問題只有Θ(mn) 個不同的子問題,我們可以使用DP來自底向上的計算。
算法僞代碼:

LCS-Length(X[],Y[]){
    int m=x.length;
    int n=Y.length;
    int[][] c = new int[m + 1][n + 1];
    char[][] b = new char[m + 1][n + 1];

    for(i=1 to m){
        c[i,0]=0;//
    }
    for(j=0 to n){
        c[0,j]=0;
    }
    /**
    i是行 j是列
    */
    for(i=1 to m){
        for(j=1 to n){
            if(xi==yj){
                c[i,j]=c[i-1,j-1]+1;
                b[i,j]='\';
            }
            elseif(c[i-1,j]>=c[i,j-1]){
                c[i,j]=c[i-1,j];
                b[i,j]='|';
            }else{
                c[i,j]=c[i,j-1];
                b[i,j]='--';
            }
        }
    }

    return c and b;


}

2.4 構造LCS(構造最優解)

我們可以使用輔助表b來快速構造x和y的LCS,只需要從b[m,n]開始按照箭頭的方向追蹤即可.遞歸算法如下:

PRINT_LCS(X,b[][],i,j){
    if(i==0 || j==0){
        return;
    }
    if(b[i,j]=="\"){
        PRINT_LCS(X,b[][],i-1,j-1);
        print xi;
    }else if(b[i,j]=="|"){
        PRINT_LCS(X,b[][],i-1,j)
    }else{
        PRINT_LCS(X,b[][],i,j-1)
    }

}

3. 動態規劃Java實現  

算法實現類

package lbz.ch15.dp.ins3;

/**
 * @author LbZhang
 * @version 創建時間:2016年3月9日 下午9:51:22
 * @description 最大公共子序列
 */
public class LCS {
    /***
     * * 這一部分我們使用輔助表,從左上角開始計算每一個位置上LCS的長度 判斷算法:
     */
    public static int[][] lcsLength(Object[] x, Object[] y) {
        int m = x.length;
        int n = y.length;

        int[][] c = new int[m + 1][n + 1];
        char[][] b = new char[m + 1][n + 1];
        int i, j;
        for (i = 1; i <= m; i++) {
            c[i][0] = 0;
        }
        for (j = 0; j <= n; j++) {
            c[0][j] = 0;
        }
        for (i = 1; i <= m; i++) {
            for (j = 1; j <= n; j++) {
                if (x[i - 1].equals(y[j - 1])) {
                    c[i][j] = c[i - 1][j - 1] + 1;
                    b[i][j]='\\';
                } else if (c[i - 1][j] >= c[i][j - 1]) {
                    c[i][j] = c[i - 1][j];
                    b[i][j]='|';
                } else {
                    c[i][j] = c[i][j - 1];
                    b[i][j]='-';
                }
            }
        }
        return c;
    }
    /**
     * 爲了輸出最長公共子序列,改進的輸出
     * @param x
     * @param y
     * @return
     */

    public static char[][] lcsPrint(Object[] x, Object[] y) {
        int m = x.length;
        int n = y.length;

        int[][] c = new int[m + 1][n + 1];
        char[][] b = new char[m + 1][n + 1];
        int i, j;
        for (i = 1; i <= m; i++) {
            c[i][0] = 0;
        }
        for (j = 0; j <= n; j++) {
            c[0][j] = 0;
        }
        for (i = 1; i <= m; i++) {
            for (j = 1; j <= n; j++) {
                if (x[i - 1].equals(y[j - 1])) {
                    c[i][j] = c[i - 1][j - 1] + 1;
                    b[i][j]='\\';
                } else if (c[i - 1][j] >= c[i][j - 1]) {
                    c[i][j] = c[i - 1][j];
                    b[i][j]='|';
                } else {
                    c[i][j] = c[i][j - 1];
                    b[i][j]='-';
                }
            }
        }
        return b;
    }

    // ///print the lcs
    // //採用遞歸的方式將結果打印出來
    public static void printLcs(int[][] c, Object[] x, Object[] y, int i, int j) {
        if (i == 0 || j == 0) {
            return;
        }
        if (x[i - 1].equals(y[j - 1])) {
            printLcs(c, x, y, i - 1, j - 1);
            System.out.print(x[i - 1] + " ");
        } else if (c[i - 1][j] >= c[i][j - 1]) {
            printLcs(c, x, y, i - 1, j);
        } else {
            printLcs(c, x, y, i, j - 1);
        }
    }

    public static void printBySignalLcs(char[][] b, Object[] x, int i, int j) {
        if (i == 0 || j == 0) {
            return;
        }
        if (b[i][j]=='\\') {
            printBySignalLcs(b,x,i-1,j-1);
            System.out.print(x[i - 1] + " ");
        } else if (b[i][j]=='|') {
            printBySignalLcs(b,x,i-1,j);
        } else {
            printBySignalLcs(b,x,i,j-1);
        }
    }
}

測試類

package lbz.ch15.dp.ins3;
/** 
 * @author LbZhang
 * @version 創建時間:2016年3月10日 下午3:19:57 
 * @description 最長公共子序列測試類
 */
public class Test {
    public static void main(String[] args) {
        Character[] x = { 'A', 'C', 'C', 'G', 'G', 'T', 'C', 'G', 'A', 'G',
                'T', 'G', 'C', 'G', 'C', 'G', 'G', 'A', 'A', 'G', 'C', 'C',
                'G', 'G', 'C', 'C', 'G', 'A', 'A' }, 
                y = { 'G', 'T', 'C', 'G',
                'T', 'T', 'C', 'G', 'G', 'A', 'A', 'T', 'G', 'C', 'C', 'G',
                'T', 'T', 'G', 'C', 'T', 'C', 'T', 'G', 'T', 'A', 'A', 'A' };
        Integer[] a = { 389, 207, 155, 300, 299, 170, 158, 65 }, b = { 389,
                300, 299, 207, 170, 158, 155, 65 };

        int[][] c;
        char[][] p;
        c = LCS.lcsLength(x, y);
        //p=LCS.lcsPrint(x, y);
        LCS.printLcs(c, x, y, 29, 28);
        System.out.println();
        c = LCS.lcsLength(a, b);//二維表
        LCS.printLcs(c, a, b, 8, 8);
        System.out.println();

        System.out.println("--------******改進******---------");
        p=LCS.lcsPrint(x, y);
        LCS.printBySignalLcs(p, x, 29, 28);
        System.out.println();
        p=LCS.lcsPrint(a, b);
        LCS.printBySignalLcs(p, a, 8, 8);
    }

}

實驗結果
最大公共子序列

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