动态规划之最长公共子序列
问题
给出两个字符串,求出这样的一
个最长的公共子序列的长度:子序列
中的每个字符都能在两个原串中找到,
而且每个字符的先后顺序和原串中的
先后顺序一致。
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