一: 作用
最长公共子序列的问题常用于解决字符串的相似度,是一个非常实用的算法,作为码农,此算法是我们的必备基本功。
二:概念
举个例子,cnblogs这个字符串中子序列有多少个呢?很显然有27个,比如其中的cb,cgs等等都是其子序列,我们可以看出
子序列不见得一定是连续的,连续的那是子串。
我想大家已经了解了子序列的概念,那现在可以延伸到两个字符串了,那么大家能够看出:cnblogs和belong的公共子序列吗?
在你找出的公共子序列中,你能找出最长的公共子序列吗?
从图中我们看到了最长公共子序列为blog,仔细想想我们可以发现其实最长公共子序列的个数不是唯一的,可能会有两个以上,
但是长度一定是唯一的,比如这里的最长公共子序列的长度为4。
三:解决方案
<1> 枚举法
这种方法是最简单,也是最容易想到的,当然时间复杂度也是龟速的,我们可以分析一下,刚才也说过了cnblogs的子序列
个数有27个 ,延伸一下:一个长度为N的字符串,其子序列有2N个,每个子序列要在第二个长度为N的字符串中去匹配,匹配一次
需要O(N)的时间,总共也就是O(N*2N),可以看出,时间复杂度为指数级,恐怖的令人窒息。
<2> 动态规划
既然是经典的题目肯定是有优化空间的,并且解题方式是有固定流程的,这里我们采用的是矩阵实现,也就是二维数组。
第一步:先计算最长公共子序列的长度。
第二步:根据长度,然后通过回溯求出最长公共子序列。
现有两个序列X={x1,x2,x3,...xi},Y={y1,y2,y3,....,yi},
设一个C[i,j]: 保存Xi与Yj的LCS的长度。
递推方程为:
不知道大家看懂了没?动态规划的一个重要性质特点就是解决“子问题重叠”的场景,可以有效的避免重复计算,根据上面的
公式其实可以发现C[i,j]一直保存着当前(Xi,Yi)的最大子序列长度。
定理: LCS的最优子结构性质
设序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一个最长公共子序列Z=<z1, z2, …, zk>,则:
其中Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>。
最长公共子序列
问题描述
最长公共子序列是一个十分实用的问题,它可以描述两段文字之间的“相似度”,即它们的雷同程度,从而能够用来辨别抄袭。对一段文字进行修改之后,计算改动前后文字的最长公共子序列,将除此子序列外的部分提取出来,这种方法判断修改的部分,往往十分准确。
最长公共子序列也称作最长公共子串(不要求连续),英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
例如,若x=和y=,则序列是x和y的一个公共子序列,序列<B,C,B,A>也是x和y的一个公 共子序列。而且,后者是x和y的一个最长公共子序列,因为x和y没有长度大于4的公共子序列。
输入
输入数据有T组测试数据。测试数据的数目 (T)在输入的第一行给出。每组测试数据有两行:每行有一个字符串,每个字符串的长度都不超过20。
输出
每个用例,用一行输出其最大公共子序列的长度,如果没有公共子序列,则输出0。
样例输入
2
abceef
1235896
ABCBDAB
BDCABA
样例输出
0
4
#include<iostream>
using namespace std;
int M[101][101]; //公共子序列数组
char a[101]; //第一个字符串,下标从1开始
char b[101]; //第二个字符串,下标从1开始
//n,m是两个字符串的长度
void work(int n,int m)
{
int i,j;
//将第一行清零
for(i=1;i<=n;i++)
M[i][0]=0;
//将第一列清零
for(j=1;j<=m;j++)
M[0][i]=0;
//填充其它格子内容
//方法是
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
if(a[i-1]==b[j-1]) //i,j从1开始,但字符串是从0开始
M[i][j]=M[i-1][j-1]+1;
//本来有三种情况,但是M[i][j]=M[i-1][j-1]可以不要添加进来
//就如下两种情况就可以啦
else
M[i][j]=M[i][j-1];
}
}
}
int main()
{
int k;
cin>>k; //输入用例个数
getchar(); //getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符也就是说,
//如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,
//但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。
while(k--)
{
gets(a); //表示从键盘接收一个字符串,并放到数组a中,
gets(b); //表示从键盘接收一个字符串,并放到数组b中,
work(strlen(a),strlen(b)); //strlen返回字符个数.
cout<<M[strlen(a)][strlen(b)]<<endl;
}
return 0;
}
ABCBDAB
BDCABA
第一个串长为7
第二个串长为6
思路如下:
各串都删除最后一个字符,则得到如下:
ABCBDA B
BDCAB A
显然删除的最后字符不相同,则原串的最长公共子序列应为:
ABCBDA B
BDCABA
和
ABCBDAB
BDCAB A
中的最长公共子序列中的某个值。
用C[n][m]表示最长公共子序列长度,则如果后两个字符不相同,C[n][m]的值应为C[n-1][m]和C[n][m-1]中最大的一个。
如果相同,例如两个原串为:
ABCBDA
BDCA
最后都为A,则原串的最长公共子序为原串都删除最后一个字符得到的最长值+1,即C[n][m]=C[n-1][m-1]+1。
由此可知,即求C[i][j],需要先求C[i-1][j-1],C[i-1][j]和C[i][j-1],即其左上角,上面和左边的值。
结合经验,显然可以对C[][]先按行,后按列求出其值,得到[n][m]即为结果。
需要注意的是C[i][j]表示只取第一个串的前i个字符和第二个串的前j个字符,即忽略后面的字符。
例:
ABCBDAB
BDCABA
得到C[][]矩阵如下:
|
|
| B | D | C | A | B | A |
| i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
A | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
B | 2 | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
C | 3 | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
B | 4 | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
D | 5 | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
A | 6 | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
B | 7 | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
行列对比,不同为0,相同则进1,再不同则,则仍为1,再相同则再进1;