文档章节

最长公共子序列(LCS)

r
 ranjiewen
发布于 2016/11/03 23:52
字数 3260
阅读 14
收藏 0
      最长公共子序列,英文缩写为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);
    }
}

 

参考:

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

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

本文转载自:http://www.cnblogs.com/ranjiewen/p/5901208.html

r
粉丝 1
博文 203
码字总数 28
作品 0
武汉
程序员
私信 提问
腾讯 2017 暑假实习生编程题(一):给定一个字符串s,你可以从中删除一些字符,使得剩下的串是一个回文串。如何删除才能使得回文串最长呢? 输出需要删除的字符个数。

题目 回文串的特点是,逆序输出和正序输出是一样的。 所以这道题可以转化为:如果将此字符串逆序输出,那么两个字符串的最长公共子序列将是最长的回文字符串,那么剩余的值将是要删除的字符个...

TinyDolphin
2017/11/22
0
0
最长公共子序列(LCS)

最长公共子序列,即Longest Common Subsequence,LCS。一个序列S任意删除若干个字符得到新序列T,则T叫做S的子序列;两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序...

klaus丶
2016/02/24
30
0
动态规划算法之:最长公共子序列 & 最长公共子串(LCS)

1、先科普下最长公共子序列 & 最长公共子串的区别: 找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。而最长公共子序列则并不要求连续。 2、最长公共子串 其实这是一个序贯决...

大数据之路
2013/03/25
0
2
LCS 算法:Javascript 最长公共子序列

最长公共子序列(Longest Common Subsequence LCS)是从给定的两个序列X和Y中取出尽可能多的一部分字符,按照它们在原序列排列的先后次序排列得到。LCS问题的算法用途广泛,如在软件不同版本...

技术小能手
2018/07/23
0
0
动态规划法(十)最长公共子序列(LCS)问题

问题介绍   给定一个序列X=X=,另一个序列Z=Z=满足如下条件时称为X的子序列:存在一个严格递增的X的下标序列,对所有的j=1,2,...,kj=1,2,...,k满足xij=zj.xij=zj.   给定两个序列XX和YY,...

jclian91
2018/06/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

如何使用pagehelper分页

<c:if test="${page != null && page.getTotal() > 0 }"> <nav style="text-align: center"><ul class="pagination pagination-lg"><li><a>共 ${page.total } 条记录</a></l......

南桥北木
2分钟前
0
0
Kafka生产者端优化

测试环境虚拟机 CPU:2核 RAM:2G Kafka Topic为1分区,1副本 Kafka生产者端发送延迟优化 batch.size batch.size 单位为字节,为了方便这里都表示为kb 默认配置,batch.size=16kb [root@10 kaf...

阿提说说
22分钟前
0
0
大数据(Hive-搭建和基本使用)

Hive背景及应用场景 Hive是什么? 由facebook开源,最初用于解决海量结构化的日志数据统计问题; ETL (Extraction-Transformation-Loading )工具 构建在Hadoop之上的数据仓库; 数据计算使...

这很耳东先生
22分钟前
0
0
大数据顶尖职位必备的9项技能

虽然对于大数据,我是很热爱,技术上也是刚入门,但是我相信通过我的不断努力,我会碰到大数据的一点皮毛的!哈哈哈!!!因为在这个大数据时代,总觉得在互联网公司里处理数据的技术工程师很...

卢小希
23分钟前
1
0
MaxCompute 费用暴涨之新增SQL分区裁剪失败

现象:因业务需求新增了SQL任务,这SQL扫描的表为分区表,且SQL条件里表只指定了一个分区,按指定的分区来看数据量并不大,但是SQL的费用非常高。费用比预想的结果相差几倍甚至10倍以上。 若...

阿里云云栖社区
23分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部