最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。
最长公共子序列
问题:对于母串X=<x1,x2,⋯,xm>
,Y=<y1,y2,⋯,yn>
,求LCS与最长公共子串。
动态规划解决:
假设Z=<z1,z2,⋯,zk>
是X与Y的LCS, 我们观察到
- 如果
xm=yn
,则zk=xm=yn
,有Zk−1
是Xm−1
与Yn−1
的LCS; - 如果
xm≠yn
,则Zk
是Xm
与Yn−1
的LCS,或者是Xm−1
与Yn
的LCS。
因此,求解LCS的问题则变成递归求解的两个子问题。但是,上述的递归求解的办法中,重复的子问题多,效率低下。改进的办法——用空间换时间,用数组保存中间状态,方便后面的计算。这就是动态规划(DP)的核心思想了。
用二维数组c[i][j]
记录串x1x2⋯xi
与y1y2⋯yj
的LCS长度,则可得到状态转移方程:
代码实现:
#include<iostream>
#include<string.h>
#include<algorithm>
#define MAX 1005
using namespace std;
int temp[MAX][MAX];
int LCS(string &s1,string &s2){
memset(temp,0,MAX*sizeof(int));
int len1 = s1.length();
int len2 = s2.length();
for(int i = 0; i < len1; i++){
for(int j = 0; j < len2; j++){
if(s1[i] == s2[j]){
temp[i+1][j+1] = temp[i][j]+1;//从下标(1,1)开始存储
}else{
temp[i+1][j+1] = max(temp[i+1][j],temp[i][j+1]);
}
}
}
return temp[len1][len2];
}
代码解释:
其实就是一个填充二维数组记录状态的过程。
假设s1='blog'
,s2='long'
则记录状态的二维数组temp为:
index(s1,s2) | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | - | - | - | - |
1 | - | 0 | 0 | 0 | 0 |
2 | - | 1 | 1 | 1 | 1 |
3 | - | 1 | 2 | 2 | 2 |
4 | - | 1 | 2 | 2 | 3 |
解释下temp:纵向为s1字符串的下标,横向为s2字符串的下标
第三行:用s1(‘blog’)的b
去匹配逐一匹配s2(‘long’),因为s2没有b
,所以记录下的公共子序列都为0.
第四行:用s1(‘blog’)的l
去匹配逐一匹配s2(‘long’),第一个匹配到了l,所以可以确定目前的最长公共子序列长度为1,然后接着匹配,o,n,g
都与l
不匹配,所以就从上方(temp[i][j+1])和左侧(temp[i+1][j])选取最大者。(原因:上方表示b
匹配到的最长公共子序列,左侧表示bl
目前匹配到的最长公共子序列)
剩下的原理都相同。
最长公共子串
考虑到子串的连续性,将二维数组c[i,j]c[i,j]用来记录具有这样特点的子串——结尾为母串x1x2⋯xi
与y1y2⋯yj
的结尾——的长度。
状态方程为:(子串只需在意之前的序列长度就行了)
代码:
#include<iostream>
#include<string.h>
#include<algorithm>
#define MAX 1005
using namespace std;
int temp[MAX][MAX];
int result = 0;;
int LCS(string &s1,string &s2){
memset(temp,0,MAX*sizeof(int));
int len1 = s1.length();
int len2 = s2.length();
for(int i = 0; i < len1; i++){
for(int j = 0; j < len2; j++){
if(s1[i] == s2[j]){
temp[i+1][j+1] = temp[i][j]+1;
result = max(temp[i+1][j+1],result);//记录下最大值
}else{
temp[i+1][j+1] = 0;
}
}
}
return result;
}