动态规划:最长公共子序列问题

最长公共子序列问题(Longest-common-subsequence Problem)简称LCS问题。题目为给定两个序列X、Y求它们的LCS(最长公共子序列),这里的子序列Z的定义为:Z中的元素既在X中也在Y中,并且他们在X、Y中满足严格的下标为一个增序列(假设下标从左到右依次增大)。另外,不要求Z中的元素在X、Y中是连续的,比如当A = {A, B, C, D, B, C, A},B = {B, C, A, D}时,可以有Z = {B, C, D}。现在我们需要求的是LCS,即符合这种条件的最长的一个公共子序列。

而LCS问题是一类动态规划(DP:dynamic programming)问题 。其具有最优子结构性质,定义如下:

设序列 X = {x1,x2,...,xm} 和 Y = {y1,y2,...yn} 的一个最长公共子序列 Z = {z1,z2,...,zk},则:
    1> 若 xm=yn,则 zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列; 
  2> 若 xm≠yn且 zk≠xm ,则 Z是 Xm-1和 Y的最长公共子序列;
    3> 若 xm≠yn且 zk≠yn ,则 Z是 X和 Yn-1的最长公共子序列;
    其中Xm-1={x1,x2,...,xm-1},Yn-1={y1,y2,...yn-1},Zk-1={z1,z2,...,zk-1}。

上面的结论很容易证明。


下面贴上算法导论中的伪代码(其中的数组b可以不用理会,c用于存储Xm和Yn的LCS):


源代码如下(注意:我的源代码中的n是序列X的长度,m是序列Y的长度):

/*  计算LCS,动态规划从前往后推算  */
#include <iostream>
#include <stdio.h>
#include <memory.h>
#define N 1000
char X[N], Y[N];                //序列X、Y
int C[N][N];                    //Cij为Xi和Yj的最长公共子序列
int lcs(int n, int m);          //计算最长公共子序列
void getAnswer(int n, int m);   //构建答案

int main()
{
    int m, n;
    printf("Enter n and m: ");
    while(2 == scanf("%d%d", &n, &m))
    {
        printf("Enter %d elements for X:", n);
        while(getchar() != '\n') continue;      //清除回车符等
        for(int i = 1; i <= n; ++i)
            scanf("%c", &X[i]);
        printf("Enter %d elements for Y:", m);
        while(getchar() != '\n') continue;
        for(int i = 1; i <= m; ++i)
            scanf("%c", &Y[i]);
        printf("MaxLength: %d\nLCS:", lcs(n, m));
        getAnswer(n, m);
        printf("\nEnter n and m: ");
    }
    return 0;
}

int lcs(int n, int m)
{
    for(int i = 0; i <= n; ++i)
        C[i][0] = 0;
    for(int i = 0; i <= m; ++i)
        C[0][i] = 0;

    for(int i = 1; i <= n; ++i)
    {
        for(int j = 1; j <= m; ++j)
        {
            if(X[i] == Y[j])
                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[n][m];
}

void getAnswer(int n, int m)
{
    if(0 == n || 0 == m)
        return;
    if((C[n][m] == C[n-1][m-1] + 1) && (X[n] == Y[m]))  //一定要判断X[n]和Y[m]是否相等
    {
        getAnswer(n-1, m-1);
        printf("%c", X[n]);     //等价于printf("%c", Y[m]);
    }
    else if(C[n][m] == C[n-1][m])
        getAnswer(n-1, m);
    else
        getAnswer(n, m-1);
}


另一种方法是从后面向前面推算,当然得到的答案也肯能不一样,但一定是最长的:

/*  计算LCS,动态规划从后往前推算  */
#include <iostream>
#include <stdio.h>
#include <memory.h>
#define N 1000
char X[N], Y[N];                                //序列X、Y
int C[N][N];                                    //Cij为Xi和Yj的最长公共子序列
int lcs(int n, int m);                          //计算最长公共子序列
void getAnswer(int x, int y, int n, int m);     //构建答案

int main()
{
    int m, n;
    printf("Enter n and m: ");
    while(2 == scanf("%d%d", &n, &m))
    {
        printf("Enter %d elements for X:", n);
        while(getchar() != '\n') continue;      //清除回车符等
        for(int i = 1; i <= n; ++i)
            scanf("%c", &X[i]);
        printf("Enter %d elements for Y:", m);
        while(getchar() != '\n') continue;
        for(int i = 1; i <= m; ++i)
            scanf("%c", &Y[i]);
        printf("MaxLength: %d\nLCS:", lcs(n, m));
        getAnswer(1, 1, n, m);
        printf("\nEnter n and m: ");
    }
    return 0;
}

int lcs(int n, int m)
{
    for(int i = 1; i <= n + 1; ++i)
        C[i][0] = 0;
    for(int i = 1; i <= m + 1; ++i)
        C[0][i] = 0;

    for(int i = n; i >= 1; --i)
    {
        for(int j = m; j >= 1; --j)
        {
            if(X[i] == Y[j])
                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[1][1];
}

void getAnswer(int x, int y, int n, int m)
{
    if(x == n + 1 || y == m + 1)
        return;
    if((C[x][y] == C[x+1][y+1] + 1) && (X[x] == Y[y]))
    {
        printf("%c", X[x]);     //等价于printf("%c", Y[y]);
        getAnswer(x+1, y+1, n, m);
    }
    else if(C[x][y] == C[x+1][y])
        getAnswer(x+1, y, n, m);
    else
        getAnswer(x, y+1, n, m);
}


如有疏漏,请批评指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值