动态规划之最长公共子序列

动态规划之最长公共子序列

问题

给出两个字符串,求出这样的一
个最长的公共子序列的长度:子序列
中的每个字符都能在两个原串中找到,
而且每个字符的先后顺序和原串中的
先后顺序一致。

Sample Input:
abcfbc abfcab
programming contest
abcd mnp

Sample Output:
4
2
0

解析

对于动态规划的问题,最重要的一步就是找出状态转移方程。那么最长公共子序列的状态方程是什么呢?我们来分析分析。
假设有两个字符串A和B:A串为a1 a2 a3 … am; B串为b1 b2 b3 …bn。
Ax为从a1开始到ax的连续字符串,By为从b1开始到by的连续字符串
假设L[x][y]为Ax和By的最长公共子串的长度。
那么,我们很容易得出:
如果ax==by,则L[x][y] = L[x-1][y-1] + 1。
如果ax和by不相等呢?那么L[x][y]就等于L[x][y-1]和L[x-1][y]中更大的那一个。具体是哪个呢?我们不必知道。因为已经有了当前状态与之前状态的关系,我们只需要给出最初状态,计算机就可以帮我们一步一步计算出结果。
容易想到:当x==0 || y==0 时,L[x][y] = 0 即当字符串A的长度为0或者字符串B的长度为0时,最长公共子序列的长度必然为0(空字符串)。
据此,就可以写出最长公共子序列的状态转移方程:

有了状态转移方程,求出最长公共子序列的长度轻而易举。
那么我们怎么求出最长公共子序列本身呢?答案其实也在状态转移方程中:我们把L想象成一个二维数组,当ax等于by时,L[x][y]只有一条路可以走:L[x-1][y-1] + 1。
所以只要当L[x][y]的值比L[x-1][y-1]大,我们就可以知道此时ax等于by。既然ax等于by了,那此时的ax和by一定是字符串A和字符串B的公共元素,也是最长子序列的公共元素。自然,最长子序列中包含它们了。如果不是走上面的路,那么L[x][y] 的值就是L[x-1][y] 与L[x][y-1]中较大的那个,此时ax不等于by。所以我们只需要比较L[x-1][y] 与L[x][y-1],较大的就是L[x][y]来时的路。就这样,根据状态转移方程和计算出的二维数组L,就可以倒推出最长公共子序列的所有元素的逆序。
值得注意的一点就是:当L[x][y] = max(L[x-1][y], L[x][y-1]) 时,我们只用了L[x-1][y] 与 L[x][y-1] 中较大的那个。如果它俩相等呢?如果相等的话,我们推出来的二维数组L的结果不会变化;但是我们倒推出的最长公共子序列就不一样了:
我们在选择L[x][y] = L[x-1][y]或者选择L[x][y] = L[x][y-1],对于一个二维数组L而言,肯定是两条不一样的路径。所以最长公共子序列的序列不一定只有一个,或者是一个,或者是2个,或者是4个…

代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define MAX 10000

struct DATA{
    char a[MAX];
    char b[MAX];    
};

typedef struct DATAS{
    int i;
    struct DATA data[];
} DATAS;

int L[MAX][MAX];

int my_max(int a, int b)
{
    return (a>b ? a : b);
}
int LCS(char a[], char b[], int m, int n)
{
    int i = 0;
    int j = 0;
    int li = i + 1;
    int lj = j + 1;
    for(i = 0; i<m; i++)
    {
        li = i + 1;
        for(j = 0; j<n; j++)
        {
            lj = j + 1;
            if(a[i] == b[j])
            {
                L[li][lj] = L[i][j] + 1;
            }
            else
            {
                L[li][lj] = my_max(L[li][j], L[i][lj]);
            }
        }
    }
    for(i = 0; i<=m; i++)
    {
        for(j = 0; j<=n; j++)
        {
            printf("%-3d", L[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    return L[m][n];

}

int main()
{
    int i = 0;
    DATAS *Datas = (DATAS *)malloc(sizeof(DATAS) + sizeof(struct DATA));
    DATAS *cache;
    FILE *fp = fopen("data.txt","rb");
    if(fp != NULL)
    {
        fscanf(fp, "%d", &(Datas->i)); //读文件的一个字符,记录数据的组数
        if(Datas->i != 0)
        {
            cache = (DATAS *)realloc(Datas, sizeof(DATAS) + (Datas->i)*sizeof(struct DATA));
            if(cache != NULL)
            {
                Datas = cache;
                cache = NULL;
                while(i<Datas->i)
                {
                    fscanf(fp, "%s%s", Datas->data[i].a, Datas->data[i].b);
                    i++;
                }
            }
            else
            {
                printf("sorry, there have not enough space to be used\n");
                return 3;
            }
        }
        else
        {
            printf("sorry, the count of data is zero\n");
            return 2;
        }
        fclose(fp);
    }
    else{
        printf("sorry, can not open the file\n");
        return 1;
    }

    for(i = 0; i<Datas->i; i++)
    {
        int m = strlen(Datas->data[i].a);
        int n = strlen(Datas->data[i].b);
        printf("%d\n", LCS(Datas->data[i].a, Datas->data[i].b, m, n));

        while(m>0 && n>0)
        {
            if (Datas->data[i].a[m-1] == Datas->data[i].b[n-1])
            {
                printf("%c ", Datas->data[i].a[m-1]);
                m--;
                n--;
            }
            else if (L[m][n] == L[m-1][n])
            {
                m--;
            }
            else n--;
        }
        printf("\n\n\n");
    }
    free(Datas);
    return 0;
}

ps: 最近复习了文件读取和柔性数组,为了巩固知识,就在解最长公共子序列的时候加上了它们。
代码中用的文件“data.txt” ,放在该C文件的相同目录下。以下是data.txt中的内容

4
abcfbc
abfcab

13456778
357486782

abcd
mnp

97862641
8761
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值