最长公共子序列

最长公共子序列(Longest Common Subsequence)(LCS)是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题

这与查找 最长公共子串 的问题不同的地方是:子序列不需要在原序列中占用连续的位置 。

最长公共子序列问题是一个经典的计算机科学问题,也是数据比较程序,比如 Diff 工具,和生物信息学应用的基础。它也被广泛地应用在版本控制,比如Git用来调和文件之间的改变

比如 abc abg bdf aeg acefg 等都是 abcdefg 的子序列

ABCDGH AEDFHR 最长公共子序列为 ADH,长度为 3

AGGTAB  和  GXTXAYB 最长公共子序列为 GTAB,长度为 4

算法过程:

以 AGGTAB  和  GXTXAYB 为例
先判断最后一个字符是否相同,相同则
L(AGGTAB,GXTXAYB) = 1 + L(AGGTA,GXTXAY)

接着判断最后一个字符是否相同,不相同则
L(AGGTA,GXTXAY) = MAX(L(AGGT,GXTXAY), L(AGGTA,GXTXA))

以此类推,直到
L(A, ) = 0
L( ,G) = 0


例子 - 1

#include <stdio.h>
#include <string.h>
#define dl_max(x,y) ((x) > (y) ? (x) : (y))
int lcs(char *s1, int n1, char *s2, int n2 ) 
{ 
    if (n1 == 0 || n2 == 0)
        return 0;

    if (s1[n1-1] == s2[n2-1]) {
        return 1 + lcs(s1,n1-1,s2,n2-1);
    }

    return dl_max(lcs(s1,n1-1,s2,n2), lcs(s1,n1,s2,n2-1));
} 

int main()
{
    char s1[] = "daileinote";
    char s2[] = "adeoeb";

    int n1 = strlen(s1);
    int n2 = strlen(s2);
    int len = lcs(s1,n1,s2,n2);

    printf("lcs len: %d\n", len);
    return 0;
}

上面的这种实现方法时间复杂度为O(2^n),而且当这两个字符串没有相同字符时出现最差情况, 有点像下面的树

                      lcs("AXYT", "AYZX")
                       /                 
         lcs("AXY", "AYZX")                lcs("AXYT", "AYZ")
         /          \                          /           \    
lcs("AX", "AYZX")   lcs("AXY", "AYZ")   lcs("AXY", "AYZ")  lcs("AXYT", "AY")

可以看出 lcs(“AXY”, “AYZ”)  实现了2次,如果我们把这个数画完,那么会有更多子问题被重复解决。

下面是利用动态规划的表格法的去解决

例子 - 2

#include <stdio.h>
#include <string.h>
#define dl_max(x,y) ((x) > (y) ? (x) : (y))

int print_lcs(char *s1, int n1, char *s2, int n2, int L[n1+1][n2+2])
{
    int index = L[n1][n2];
    char lcs[index+1];
    lcs[index--] = 0;

    while (n1 > 0 && n2 > 0) {
        if (s1[n1-1] == s2[n2-1]) {
            lcs[index--] = s1[n1-1];
            n1--;
            n2--;
        } else if (L[n1-1][n2] > L[n1][n2-1]) {
            n1--;
        } else
            n2--;
    }

    printf("lcs: %s\n", lcs);
} 

int lcs(char *s1, int n1, char *s2, int n2 ) 
{
    int i,j;
    int L[n1+1][n2+1];

    for (i = 0; i <=n1; i++) {
        for (j = 0; j <= n2; j++) {
            if (i == 0 || j == 0)
                L[i][j] = 0;
            else if (s1[i-1] == s2[j-1])
                L[i][j] = 1 + L[i-1][j-1];
            else
                L[i][j] = dl_max(L[i-1][j], L[i][j-1]);
        }
    }

    
    //打印
    int index = L[n1][n2];
    char lcs[index+1];
    lcs[index--] = 0;

    int kinds = 1;

    i = n1;
    j = n2;
    while (i > 0 && j > 0) {
        if (s1[i-1] == s2[j-1]) {
            lcs[index--] = s1[i-1];
            i--;
            j--;
        } else if (L[i-1][j] > L[i][j-1]) {
            i--;
        } else if (L[i-1][j] < L[i][j-1]) {
            j--;
        } else {
            //两个方向都可以,这里我们选择其中一种
            i--;
        }
    }
    printf("lcs: %s\n", lcs);

    return L[n1][n2];
}

int main()
{
    char s1[] = "daileinote";
    char s2[] = "adeoeb";

    int n1 = strlen(s1);
    int n2 = strlen(s2);
    int len = lcs(s1,n1,s2,n2);

    printf("lcs len: %d\n", len);
    return 0;
}

时间复杂度为 O(mn),比例子1好很多


参考:

geeksforgeeks

wiki-Longest_common_subsequence_problem

上一篇: 最长上升子序列
下一篇: 最小路径开销
作者邮箱: 203328517@qq.com