动态规划之最长公共子序列

这个世界上根本就不存在“不会做”这回事,当你失去了所有的依靠的时候,自然就什么都会了。

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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章