zoukankan      html  css  js  c++  java
  • 最长公共子序列 (LCS) 详解+例题模板(全)(转)

    1.摘要:
    继上篇最长上升子序列后,本篇主要讲述最长公共子序列 (LCS) 。

    2.LCS定义:
           最长公共子序列,英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。

           如果觉得抽象不好理解,那么咱们还是采用学习LIS的时候的方式。首先,让我们先来看一下子串、子序列还有公共子序列的概念(在上篇LIS中也曾涉及过) ,我们以字符子串和字符子序列为例,更为形象,也能顺带着理解字符的子串和子序列:

         (1)字符子串:指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。

         (2)字符子序列:指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。

           (3)  公共子序列:如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。如对序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 来说,序列1,8,7是它们的一个公共子序列。

           那么现在,我们再通俗的总结一下最长公共子序列(LCS):就是A和B的公共子序列中长度最长的(包含元素最多的)
    仍然用序列1,3,5,4,2,6,8,7和序列1,4,8,6,7,5为例,它们的最长公共子序列有1,4,8,7和1,4,6,7两种,但最长公共子序列的长度是4。由此可见,最长公共子序列(LCS)也不一定唯一。

          请大家用集合的观点来理解这些概念,子序列、公共子序列以及最长公共子序列都不唯一,所以我们通常特判取一个最长公共子序列,但很显然,对于固定的两个数组,虽然最LCS不一定唯一,但LCS的长度是一定的。查找最长公共子序列与查找最长公共子串的问题不同的地方在于:子序列不需要在原序列中占用连续的位置。最长公共子串(要求连续)和最长公共子序列是不同的。

    那么该如何求出两个序列的最长公共子序列长度呢?请继续往下看~

    3.LCS长度求法:
           你首先能想到的恐怕是暴力枚举?那我们先来看看:序列A有 2^n 个子序列,序列B有 2^m 个子序列,如果任意两个子序列一一比较,比较的子序列高达 2^(n+m) 对,这还没有算具体比较的复杂度。或许你说,只有长度相同的子序列才会真正进行比较。那么忽略空序列,我们来看看:对于A长度为1的子序列有C(n,1)个,长度为2的子序列有C(n,2)个,……长度为n的子序列有C(n,n)个。对于B也可以做类似分析,即使只对序列A和序列B长度相同的子序列做比较,那么总的比较次数高达:C(n,1)*C(m,1)*1 + C(n,2) * C(m,2) * 2+ …+C(n,p) * C(m,p)*p,其中p = min(m, n)。

           吓着了吧?怎么办?我们试试使用动态规划算法!

           我们用Ax表示序列A的连续前x项构成的子序列,即Ax= a1,a2,……ax, By= b1,b2,……by, 我们用LCS(x, y)表示它们的最长公共子序列长度,那原问题等价于求LCS(m,n)。为了方便我们用L(x, y)表示Ax和By的一个最长公共子序列。让我们来看看如何求LCS(x, y)。我们令x表示子序列考虑最后一项

    (1) Ax = By

             那么它们L(Ax, By)的最后一项一定是这个元素!

           为什么呢?为了方便,我们令t = Ax = By, 我们用反证法:假设L(x,y)最后一项不是t,则要么L(x,y)为空序列(别忘了这个),要么L(x,y)的最后一项是Aa=Bb ≠ t, 且显然有a < x, b < y。无论是哪种情况我们都可以把t接到这个L(x,y)后面,从而得到一个更长的公共子序列。矛盾!
           如果我们从序列Ax中删掉最后一项ax得到Ax-1,从序列By中也删掉最后一项by得到By-1,(多说一句角标为0时,认为子序列是空序列),则我们从L(x,y)也删掉最后一项t得到的序列是L(x – 1, y - 1)。为什么呢?和上面的道理相同,如果得到的序列不是L(x - 1, y - 1),则它一定比L(x - 1, y - 1)短(注意L(,)是个集合!),那么它后面接上元素t得到的子序列L(x,y)也比L(x - 1, y - 1)接上元素t得到的子序列短,这与L(x, y)是最长公共子序列矛盾。因此L(x, y) = L(x - 1, y - 1) 最后接上元素t,LCS(Ax, By) = LCS(x - 1, y - 1) + 1。

    (2)  Ax ≠ By

            仍然设t = L(Ax, By), 或者L(Ax, By)是空序列(这时t是未定义值不等于任何值)。则t  ≠ Ax和t  ≠ By至少有一个成立,因为t不能同时等于两个不同的值嘛!

    (2.1)如果t  ≠ Ax,则有L(x, y)= L(x - 1, y),因为根本没Ax的事嘛。

                LCS(x,y) = LCS(x – 1, y)
    (2.2)如果t  ≠ By,l类似L(x, y)= L(x , y - 1)

                LCS(x,y) = LCS(x, y – 1)
           可是,我们事先并不知道t,由定义,我们取最大的一个,因此这种情况下,有LCS(x,y) = max(LCS(x – 1, y) , LCS(x, y – 1))。看看目前我们已经得到了什么结论:


    LCS(x,y) = 
    (1) LCS(x - 1,y - 1) + 1      (Ax = By)
    (2) max(LCS(x – 1, y) , LCS(x, y – 1))    (Ax ≠ By)

    这时一个显然的递推式,光有递推可不行,初值是什么呢?显然,一个空序列和任何序列的最长公共子序列都是空序列!所以我们有:

    LCS(x,y) = 
    (1) LCS(x - 1,y - 1) + 1 如果Ax = By
    (2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By
    (3) 0 如果x = 0或者y = 0

    到此我们求出了计算最长公共子序列长度的递推公式。我们实际上计算了一个(n + 1)行(m + 1)列的表格(行是0..n,列是0..m),也就这个二维度数组LCS(,)。

    大概的伪代码如下:
    输入序列A, B长度分别为n,m,计算二维表 LCS(int,int):

    for x = 0 to n do
        for y = 0 to m do
            if (x == 0 || y == 0) then 
                LCS(x, y) = 0
            else if (Ax == By) then
                LCS(x, y) =  LCS(x - 1,y - 1) + 1
            else 
                LCS(x, y) = ) max(LCS(x – 1, y) , LCS(x, y – 1))
            endif
        endfor
    endfor

    注意: 我们这里使用了循环计算表格里的元素值,而不是递归,如果使用递归需要已经记录计算过的元素,防止子问题被重复计算。

    现在问题来了,我们如何得到一个最长公共子序列而仅仅不是简单的长度呢?其实我们离真正的答案只有一步之遥!

    仍然考虑那个递推式,我们LCS(x,y)的值来源的三种情况:


    (1) LCS(x – 1,  y – 1) + 1如果Ax = By
    这对应L(x,y) = L(x,- 1 y- 1)末尾接上Ax


    (2.1) LCS(x – 1, y)  如果Ax ≠ By且LCS(x – 1, y) ≥LCS(x, y – 1)
    这对应L(x,y)= L(x – 1, y)
    (2.2) LCS(x, y – 1)  如果Ax ≠ By且LCS(x – 1, y) <LCS(x, y – 1)
    这对应L(x,y) = L(x, y – 1)


    (3) 0 如果 x =0或者y = 0
    这对应L(x,y)=空序列


    注意(2.1)和(2.2) ,当LCS(x – 1, y) = LCS(x, y – 1)时,其实走哪个分支都一样,虽然长度时一样的,但是可能对应不同的子序列,所以最长公共子序列并不唯一。
    神奇吧?又一个类似的递推公式。可见我们在计算长度LCS(x,y)的时候只要多记录一些信息,就可以利用这些信息恢复出一个最长公共子序列来。就好比我们在迷宫里走路,走到每个位置的时候记录下我们时从哪个方向来的,就可以从终点回到起点一样。

    另外,说一下复杂度?时间复杂度时O(n * m),空间也是O(n * m)。

    4.LCS经典例题模板:
    例1:Common Subsequence(求LCS长度)
    Description
    A subsequence of a given sequence is the given sequence with some elements (possible none) left out. Given a sequence X = <x1, x2, ..., xm> another sequence Z = <z1, z2, ..., zk> is a subsequence of X if there exists a strictly increasing sequence <i1, i2, ..., ik> of indices of X such that for all j = 1,2,...,k, xij = zj. For example, Z = <a, b, f, c> is a subsequence of X = <a, b, c, f, b, c> with index sequence <1, 2, 4, 6>. Given two sequences X and Y the problem is to find the length of the maximum-length common subsequence of X and Y. 
    The program input is from a text file. Each data set in the file contains two strings representing the given sequences. The sequences are separated by any number of white spaces. The input data are correct. For each set of data the program prints on the standard output the length of the maximum-length common subsequence from the beginning of a separate line. 

    Sample Input
    abcfbc abfcab
    programming contest
    abcd mnp
    Sample Output
    4
    2
    0
    思路:

    题意是,称序列 Z = < z1, z2, ..., zk >是序列X = < x1, x2, ..., xm >的子序列当且仅当存在严格上升的序列< i1, i2, ..., ik >,使得对j = 1, 2, ... ,k, 有xij = zj。比如Z = < a, b, f, c > 是X = < a, b,c, f, b, c >的子序列。现在给出两个序列X 和Y,你的任务是找到X 和Y 的最大公共子序列,也就是说要找到一个最长的序列Z,使得Z 既是X 的子序列也是Y 的子序列。

    其实就是模板题啦~求LCS长度,当A[i]=A[j]时d(I,j)d(i-1,j-1)+1,否则d(i,j)=max{ d(i-1,j),d(i,j-1) },时间复杂度为O(n*m),其中n和m分别是序列A和B的长度。

    代码:

    #include <cmath>
    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <algorithm>
     
    using namespace std;
     
    char a[1001], b[1001];
    int dp[1001][1001], len1, len2;
     
    void lcs(int i,int j)
    {
        for(i=1; i<=len1; i++)
        {
            for(j=1; j<=len2; j++)
            {
                if(a[i-1] == b[j-1])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else if(dp[i-1][j] > dp[i][j-1])
                    dp[i][j] = dp[i-1][j];
                else
                    dp[i][j] = dp[i][j-1];
            }
        }
    }
     
    int main()
    {
        while(~scanf(" %s",a))
        {
            scanf(" %s", b);
            memset(dp, 0, sizeof(dp));
            len1 = strlen(a);
            len2 = strlen(b);
            lcs(len1, len2);
            printf("%d
    ", dp[len1][len2]);
        }
        return 0;
    }

    例2:最长公共子序列Lcs(求LCS具体是什么)
    Description
    给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的)。

    比如两个串为:abcicba 和 abdkscab,则 ab是两个串的子序列,abc也是,abca也是,其中abca是这两个字符串最长的子序列。

    Input
    第1行:字符串A 
    第2行:字符串B 
    (A,B的长度 <= 1000)

    Output
    输出最长的子序列,如果有多个,随意输出1个。

    Sample Input
    abcicba
    abdkscab
    Sample Output
    abca
    思路:

           此题的切入点就是动态规划,通过动归来确定哪些字符是最长公共子序列中的字符,mat[i][j] 表示第一个序列的前i个字符和第二个序列的前j个字符的公共子序列,动态转移方程为:

     dp[i][j] = max(dp[i-1][j], dp[i][j-1],dp[i-1][j-1] + (A[i]==B[j] ? 1 : 0)),表示在这三种状态中取到最大值,

    (1)第一种状态表示不录入第一个序列的第i个字符时的最长公共子序列,

    (2)第二种状态表示不录入第二个序列的第j个字符时的最长公共子序列,

    (3)第三种状态表示第一个序列的前i-1个字符与第二个序列前j-1个字符的公共子序列加上最后一个字符的录入状态,如果最后的一个字符相等则录入状态为1,否则为0。

    然后根据动归的状态,来判断我们要求得的序列中的字符有哪些。

    代码:

    #include<queue>
    #include<cmath>
    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    #define INF 0x3f3f3f3f
     
    using namespace std;
     
    char a[1001], b[1001];
    int dp[1001][1001], len1, len2;
     
    void lcs(int i, int j)
    {
        for(i=1; i<=len1; i++)
        {
            for(j=1; j<=len2; j++)
            {
                if(a[i-1] == b[j-1])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else if(dp[i-1][j] > dp[i][j-1])
                    dp[i][j] = dp[i-1][j];
                else
                    dp[i][j] = dp[i][j-1];
            }
        }
    }
     
    void llcs()
    {
        int i, j, z = 0;
        char c[1001];
        memset(c, 0, sizeof(c));
        i = len1, j = len2;
        while(i!=0 && j!=0)
        {
            if(a[i-1] == b[j-1])
            {
                c[z++] = a[--i];
                j--;
            }
            else if(dp[i-1][j] < dp[i][j-1])
                j--;
            else if(dp[i][j-1] <= dp[i-1][j])
                i--;
        }
        for(i=z-1; i>=0; i--)
            printf("%c", c[i]);
        printf("
    ");
     
    }
     
    int main()
    {
        while(~scanf(" %s", a))
        {
            scanf(" %s", b);
            memset(dp, 0, sizeof(dp));
            len1 = strlen(a);
            len2 = strlen(b);
            lcs(len1, len2);
            llcs();
        }
        return 0;
    }

    例3:Advanced Fruits(根据LCS将两个词拼接)
    Decription
    The company "21st Century Fruits" has specialized in creating new sorts of fruits by transferring genes from one fruit into the genome of another one. Most times this method doesn't work, but sometimes, in very rare cases, a new fruit emerges that tastes like a mixture between both of them. 
    A big topic of discussion inside the company is "How should the new creations be called?" A mixture between an apple and a pear could be called an apple-pear, of course, but this doesn't sound very interesting. The boss finally decides to use the shortest string that contains both names of the original fruits as sub-strings as the new name. For instance, "applear" contains "apple" and "pear" (APPLEar and apPlEAR), and there is no shorter string that has the same property. 
    A combination of a cranberry and a boysenberry would therefore be called a "boysecranberry" or a "craboysenberry", for example. 
    Your job is to write a program that computes such a shortest name for a combination of two given fruits. Your algorithm should be efficient, otherwise it is unlikely that it will execute in the alloted time for long fruit names. 

    Input
    Each line of the input contains two strings that represent the names of the fruits that should be combined. All names have a maximum length of 100 and only consist of alphabetic characters. 
    Input is terminated by end of file. 

    Output
    For each test case, output the shortest name of the resulting fruit on one line. If more than one shortest name is possible, any one is acceptable. 

    Sample Input
    apple peach
    ananas banana
    pear peach
    Sample Output
    appleach
    bananas
    pearch
    思路:

    在LCS的基础之上加上路径记录,生成dp数组的时候做上标记,之后按顺序输出结果字符串。注意还要考虑一下没有公共子序列的情况。

    代码:

    #include<queue>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
     
    using namespace std;
     
    char a[1001], b[1001], s[10000];
    int dp[1001][1001], len1, len2, k = 0;
     
    void lcs(int i,int j)
    {
        for(i=1; i<=len1; i++)
        {
            for(j=1; j<=len2; j++)
            {
                if(a[i-1] == b[j-1])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else if(dp[i-1][j] > dp[i][j-1])
                    dp[i][j] = dp[i-1][j];
                else
                    dp[i][j] = dp[i][j-1];
            }
        }
    }
     
    void llcs()
    {
        int i, j, z = 0, k = 0;
        i = len1, j = len2;
        while(dp[i][j])
        {
            if(a[i-1] == b[j-1])
            {
                s[k++]=a[--i];
                j--;
            }
            else if(dp[i][j-1] < dp[i-1][j])
                s[k++] = a[--i];
            else if(dp[i][j-1] >= dp[i-1][j])
                s[k++] = b[--j];
        }
        while(i != 0)
            s[k++] = a[--i];
        while(j!=0)
            s[k++] = b[--j];
        for(z=k-1; z>=0; z--)
            printf("%c", s[z]);
        printf("
    ");
    }
     
    int main()
    {
        while(~scanf(" %s",a))
        {
            scanf(" %s", b);
            memset(dp, 0, sizeof(dp));
            len1 = strlen(a);
            len2 = strlen(b);
            lcs(len1, len2);
            llcs();
        }
        return 0;
    }

    5.相关知识:( 建议放在一起比较区分 )

    1)最长上升子序列  ( LIS )  戳这里

    2)最长回文子串 and 最长回文子序列  (LPS)  戳这里 

    转自:https://blog.csdn.net/lxt_lucia/article/details/81209962

  • 相关阅读:
    现在连Linux都搞不懂,当初我要是这么学习操作系统就好了!
    一时技痒,撸了个动态线程池,源码放Github了
    Java线程池ThreadPoolExecutor使用和分析(一)
    canch----1.对缓存的思考
    1.java 内存数据库--H2数据库使用指南
    What’s the difference between persist, save, merge and update? Which one should you use?
    primary key's generator in JPA entity
    STM32F103驱动M24256 256k存储芯片进行读写
    【Proteus+51单片机学习笔记】-51/52系列单片机简介
    【STM32项目笔记】STM32CubeMX+Keil+Proteus联合实现LED闪烁
  • 原文地址:https://www.cnblogs.com/zl1991/p/13071407.html
Copyright © 2011-2022 走看看