ACM-最长公共子序列

最长公共子序列(The longest common subsequence),即LCS,就是说对于给定的两个字符串,它们之间的相同序列的最大长度是多少,注意序列的意思是不一定连续,但是不能无序,比如字符串ABCBDAB和BDCABA,它们之间的LCS就是BDAB、BCAB、BCBA,它们的长度都是4。

那如何求解两个字符串之间的LCS,一般可以考虑两种解法,即穷举法和动态规划法。

1、穷举法

既然是要判断子序列是否相等,那自然想到的就是吧所有子序列都构造出来,然后一一判断即可。然后分析一下复杂度,对于一个字符串,要枚举其所有的子序列,可以这么考虑,对于其每一位上的字符,无非两种情况,选或不选,那么假如字符串长度为n,那么子序列的个数,也就是枚举的复杂度就是2^n。同样对于第二个字符串,枚举其所有子序列的复杂度也是2^n。再加上最后比较两个子序列的复杂度,那么总的时间复杂度也就是O(2^n*2^n*n)。这是指数级别的复杂度,增长速度非常快。举个例子来说明其效率问题,比如对于下面的数据,用下面的穷举代码进行求解,可以发现最后一个例子已经运行不出来了,可以大概算下运行时间,一般普通计算机每秒的操作次数为10^7,那么对于最后一个例子来说,运行时间大概是2^100*2^100*100/10^7=10^55秒,化成年为单位就可以看出有多么大了:10^55/60/60/24/365=10^47年,是个天文数字。可见穷举法的效率是很低的,已经完全不可行。

acbd
abcd
ABCBDAB
BDCABA
0123456789
0123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789

#include <cstdio>
#include <cstring>
#include <algorithm>

const int MAXN = 1e3 + 5;
char text[MAXN], temp[MAXN];

// calculate the LCS of test and temp by enume
int FlagA[MAXN], FlagB[MAXN];
int LCSLen, LenA, LenB;

void LCS_EnumeB(int len1, int len2, int deep)
{
    if(len2 > len1) return;
    if(deep == LenB+1)
    {
        if(len1 != len2) return;
        if(len1 < LCSLen) return;
        int i=1, posA=1, posB=1;
        for(; i<=len1; ++i)
        {
            while(FlagA[posA] == 0) ++posA;
            while(FlagB[posB] == 0) ++posB;
            if(text[posA] != temp[posB]) break;
            ++posA;
            ++posB;
        }
        if(i > len1)
        {
            LCSLen = len1;
        }
        return;
    }
    FlagB[deep] = 1;
    LCS_EnumeB(len1, len2+1, deep+1);
    FlagB[deep] = 0;
    LCS_EnumeB(len1, len2, deep+1);
}

void LCS_EnumeA(int len1, int deep)
{
    if(deep == LenA+1)
    {
        if(len1 == 0) return;
        memset(FlagB, 0, sizeof(FlagB));
        LCS_EnumeB(len1, 0, 1);
        return;
    }
    FlagA[deep] = 1;
    LCS_EnumeA(len1+1, deep+1);
    FlagA[deep] = 0;
    LCS_EnumeA(len1, deep+1);
}

int main()
{//freopen("sample.txt", "r", stdin);
    while(~scanf("%s%s", text+1, temp+1))
    {
        LCSLen = 0;
        LenA = strlen(text+1);
        LenB = strlen(temp+1);
        memset(FlagA, 0, sizeof(FlagA));
        LCS_EnumeA(0, 1);
        printf("the length of lcs of %s and %s is %d\n", text+1, temp+1, LCSLen);
    }
    return 0;
}


2、动态规划法

可以明显的发现,穷举法一直在重复计算子序列,这是重叠子问题,动态规划正是为了优化这类问题的算法。先看下面的转移方程。

c[i,j]代表的是第一个字符串长度为i的前缀与第二个字符串长度为j的前缀的LCS,那么可以判断上面的转移方程的正确性无疑是对的。唯一需要注意的是第三种转移方式,如果要保证状态转移成功,则要求在计算c[i,j]的时候,c[i,j-1]和c[i-1,j]都是已经计算出来了的,其实这个条件可以有递推顺序来保证,即假如两个字符串的长度分别是n和m的话,那么可以从1-n枚举i,再从1-m枚举j。最后分析下时间复杂度,很明显的两层for循环,O(n*m)。可以看到,对于同样的测试例子,这个算法都能很快的求解出来。

最后再说一下如何在上述dp算法求解完后再构造出具体的最长公共子序列。以字符串ABCBDAB和BDCABA为例,上述算法完成后,转移状态,也就是前面的c矩阵的情况如下。

图中的箭头就是dp算法进行中状态的转移方向,那么可以将这些方向记录下来,当算法完成后,由最后一个状态,也就是右下角的c[n,m]往回走,这样就可以构造出所有的最长公共子序列了。

#include <cstdio>
#include <cstring>
#include <algorithm>

const int MAXN = 1e3 + 5;
char text[MAXN], temp[MAXN];
// C[i][j] represent the LCS of prefix of text[i] and temp[j]
int C[MAXN][MAXN];
// the direction of state transition
int dir[MAXN][MAXN];

// calculate the LCS of test and temp by dp
int LCS_DP(char *text, char *temp)
{
    int n = strlen(text+1);
    int m = strlen(temp+1);
    memset(C, 0, sizeof(C));
    memset(dir, 0, sizeof(C));

    // initialize the border
    for(int i=0; i<=n; ++i) C[i][0] = 0;
    for(int i=0; i<=m; ++i) C[0][i] = 0;

    // the state transition
    for(int i=1; i<=n; ++i) for(int j=1; j<=m; ++j)
    {
        if(text[i] == temp[j])
        {
            C[i][j] = C[i-1][j-1] + 1;
            // left-up
            dir[i][j] = 1;
        }
        else if(C[i][j-1] > C[i-1][j])
        {
            C[i][j] = C[i][j-1];
            // left
            dir[i][j] = 2;
        }
        else if(C[i][j-1] < C[i-1][j])
        {
            C[i][j] = C[i-1][j];
            // up
            dir[i][j] = 3;
        }
        else
        {
            // C[i][j-1] == C[i-1][j]
            C[i][j] = C[i][j-1];
            // left or up
            dir[i][j] = 4;
        }
    }
    return C[n][m];
}

// reverse backtracking to find the lcs
void FindLCS(int i, int j, char *lcs, int curlen)
{
    if(i==0 || j==0)
    {
        for(int k=curlen-1; k>=0; --k) printf("%c", lcs[k]);
        putchar('\n');
        return;
    }
    if(dir[i][j] == 1)
    {
        // text[i] == temp[j]
        lcs[curlen] = text[i];
        FindLCS(i-1, j-1, lcs, curlen+1);
    }
    else if(dir[i][j] == 2)
    {
        // from left
        FindLCS(i, j-1, lcs, curlen);
    }
    else if(dir[i][j] == 3)
    {
        // from up
        FindLCS(i-1, j, lcs, curlen);
    }
    else
    {
        // from left or up
        FindLCS(i, j-1, lcs, curlen);
        FindLCS(i-1, j, lcs, curlen);
    }
}


int main()
{//freopen("sample.txt", "r", stdin);
    while(~scanf("%s%s", text+1, temp+1))
    {
        int lenlcs = LCS_DP(text, temp);
        int n = strlen(text+1);
        int m = strlen(temp+1);
        puts("the matrix of state transition is£º");
        for(int i=1; i<=n; ++i) for(int j=1; j<=m; ++j)
        {
             printf("%d%c", C[i][j], j==m?'\n':' ');
        }
        printf("the length of lcs of %s and %s is %d\n", text+1, temp+1, lenlcs);

        printf("the lcs of %s and %s is£º\n", text+1, temp+1);
        char lcs[MAXN];
        FindLCS(n, m, lcs, 0);

    }
    return 0;
}


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值