举个例子,如:有两条随机序列,如 1 3 4 5 5 ,and 2 4 5 5 7 6,则它们的最长公共子序列便是:4 5 5。
注意最长公共子串(Longest CommonSubstring)和最长公共子序列(LongestCommonSubsequence, LCS)的区别: 子串(Substring)是串的一个连续的部分,子序列
(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;更简略地说,前者(子串)的字符的位置必须连续,后者(子序列LCS)则不必。比如字符串acdfg 同akdfc 的最长公共子串为df,而他们的最长公共子序列是adf。LCS 可以使用动态规划法解决。
一、最长公共子串
LCS问题就是求两个字符串最长公共子串的问题。解法就是用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。然后求出对角线最长的1序列,其对应的位置就是最长匹配子串的位置.
连续i子串的特点就是如果str1[i]和str2[j]是属于某公共子串的最后一个字符,那么一定有str1[i]=str2[j] && str1[i-1] = str2[j-1],从矩阵中直观的看,就是由“1”构成的“斜线”代表的序列都是公共子串,那么最长公共子串肯定就是斜线“1”最长的那个串。
那么现在问题就可以转化了,只要构造出如上的一个矩阵,用n^2的时间就可以得到矩阵,然后再到矩阵中去寻找最长的那个“1”构成的斜线就可以了!那么,现在又有了新的问题?如何快速的找到那个“1”构成的最长斜线呢?
采用DP的思想,如果str1[i] = str2[j],那么此处的包含str1[i] 和 str2[j]公共子串的长度必然是包含str1[i-1]和str2[j-1]的公共子串的长度加1,那么现在我们可以重新定义lcs(i,j),即是lcs(i,j) = lcs(i-1,j-1) + 1,反之,lcs(i,j) = 0。那么上面的矩阵就变成了如下的样子:
下面是字符串21232523311324和字符串312123223445的匹配矩阵,前者为X方向的,后者为Y方向的。不难找到,红色部分是最长的匹配子串。通过查找位置我们得到最长的匹配子串为:21232
0 0 0 1 0 0 0 1 1 0 0 1 0 0 00 1 0 0 0 0 0 0 0 1 1 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0
0 1 0 0 0 0 0 0 0 1 1 0 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0
0 0 0 1 0 0 0 1 1 0 0 1 0 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0
0 0 0 1 0 0 0 1 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
但是在0和1的矩阵中找最长的1对角线序列又要花去一定的时间。通过改进矩阵的生成方式和设置标记变量,可以省去这部分时间。下面是新的矩阵生成方式:
0 0 0 1 0 0 0 1 1 0 0 1 0 0 0
0 1 0 0 0 0 0 0 0 2 1 0 0 0 0
1 0 2 0 1 0 1 0 0 0 0 0 1 0 0
0 2 0 0 0 0 0 0 0 1 1 0 0 0 0
1 0 3 0 1 0 1 0 0 0 0 0 1 0 0
0 0 0 4 0 0 0 2 1 0 0 1 0 0 0
1 0 1 0 5 0 1 0 0 0 0 0 2 0 0
1 0 1 0 1 0 1 0 0 0 0 0 1 0 0
0 0 0 2 0 0 0 2 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
当字符匹配的时候,我们并不是简单的给相应元素赋上1,而是赋上其左上角元素的值加一。我们用两个标记变量来标记矩阵中值最大的元素的位置,在矩阵生成的过程中来判断当前生成的元素的值是不是最大的,据此来改变标记变量的值,那么到矩阵完成的时候,最长匹配子串的位置和长度就已经出来了。
在Lintcde中下列代码AC www.lintcode.com/en/problem/longest-common-substring/#
注意实际编程中,lcs数组0位置空出来,表示一个字符都没有选,这样方便初始化!!
class Solution {
public:
/**
* @param A, B: Two string.
* @return: the length of the longest common substring.
*/
int longestCommonSubstring(string &A, string &B) {
// write your code here
int lena = A.size();
int lenb = B.size();
vector<vector<int> > lcs(lena+1, vector<int>(lenb + 1,0));
int maxLcs = 0;
for (int i = 1; i <= lena; i++){
for (int j = 1; j <= lenb; j++){
if (A[i - 1] == B[j - 1]){
lcs[i][j] = lcs[i - 1][j - 1] + 1;
}
else{
lcs[i][j] = 0;
}
if (lcs[i][j] > maxLcs){
maxLcs = lcs[i][j];
}
}
}
return maxLcs;
}
};
空间可以更节省
#include <iostream>
using namespace std;
int LCS(char *s1,int length1,char *s2,int length2){
int *tmp=new int[length2+1];
memset(tmp,0,(length2+1)*sizeof(char));
int max=-0x3f3f3f3f;
int end=0;
for(int i=1;i<=length1;i++){
for(int j=length2;j>=1;j--){
if(s1[i]==s2[j]){
tmp[j]=tmp[j-1]+1;
}else{
tmp[j]=0;
}
if(tmp[j]>max){
max=tmp[j];
end=j;
}
}
}
int start=end-max;
for(int i=start;i<end;i++){
cout<<s2[i]<<" ";
}
cout<<endl;
delete []tmp;
return max;
}
int main(){
char s1[]={'a','b','a','c','a','b','a'};
char s2[]={'c','a','b','a'};
int length1=7;
int length2=4;
cout<<LCS(s1,length1,s2,length2)<<endl;
system("pause");
return 0;
}
这段代码的关键是为什
么
for(int j=length2;j>=1;j--)是因为需要左上角的值,如果从左至右的遍历的话,仅用一维数组满足不了要求,因为求tmp[j]需要tmp[j-1],而tmp[j-1]
在前面已经被修改。从右至左就可以避免这个问题。(这就是滚动数组)
二、最长公共子序列
记:
Xi=﹤x1,⋯,xi﹥即X 序列的前i 个字符 (1≤i≤m)(前缀)
Yj=﹤y1,⋯,yj﹥即Y 序列的前j 个字符 (1≤j≤n)(前缀)
假定Z=﹤z1,⋯,zk﹥∈LCS(X , Y)。
若xm=yn(最后一个字符相同),则不难用反证法证明:该字符必是X 与Y 的任一最长公共子序列Z(设长度为k)的最后一个字符,即有zk = xm = yn 且显然有Zk-1∈LCS(Xm-1 , Yn-1)即Z 的前缀Zk-1 是Xm-1 与Yn-1 的最长公共子序列。此时,问题化归成求Xm-1 与Yn-1 的LCS(LCS(X , Y)的长度等于LCS(Xm-1 , Yn-1)的长度加1)。
若xm≠yn,则亦不难用反证法证明:要么Z∈LCS(Xm-1, Y),要么Z∈LCS(X , Yn-1)。由于zk≠xm 与zk≠yn 其中至少有一个必成立,若zk≠xm 则有Z∈LCS(Xm-1 , Y),类似
的,若zk≠yn 则有Z∈LCS(X , Yn-1)。此时,问题化归成求Xm-1 与Y 的LCS 及X 与Yn-1 的LCS。LCS(X , Y)的长度为:max{LCS(Xm-1 , Y)的长度, LCS(X , Yn-1)的长度}。
1.简单
最初用递归实现
#include <iostream>
using namespace std;
int max(int a,int b){
//a!=b
if(a>b){
return a;
}
else return b;
}
int LCS(char *s1,int index1,char *s2,int index2){
if(index1<0||index2<0){
return 0;
}else{
if(s1[index1]==s2[index2]){
//cout<<s1[index1]<<" ";
return LCS(s1,index1-1,s2,index2-1)+1;
}else{
return max(LCS(s1,index1-1,s2,index2),LCS(s1,index1,s2,index2-1));
}
}
}
int main(){
char s1[]={'a','c','d','f','g'};
char s2[]={'a','k','d','f','c'};
int index1=4;
int index2=4;
cout<<endl<<LCS(s1,index1,s2,index2)<<endl;
system("pause");
return 0;
}
结果是3,正确没问题。递归实现的弱点就在于: 重复计算很多子问题。
如果先将子问题的解都计算出现,存储起来,就可以节省很多时间
#include <iostream>
using namespace std;
int LCS(char *s1,int length1,char *s2,int length2){
int lcs[50][50]={0};
/************************************************************************/
/* 此时若理解为数组从1开始,0不用,就不会产生诸多的问题
/************************************************************************/
for(int i=1;i<=length1;i++){
for(int j=1;j<=length2;j++){
if(s1[i]==s2[j]){
lcs[i][j]=lcs[i-1][j-1]+1;
b[i][j]=1;
}else if(lcs[i-1][j]>lcs[i][j-1]){
lcs[i][j]=lcs[i-1][j];
b[i][j]=2;
}else{
lcs[i][j]=lcs[i][j-1];
b[i][j]=3;
}
}
}
return lcs[length1][length2];
}
int main(){
/*a c d f g
a k d f c*/
int length1,length2;
cin>>length1>>length2;
char *s1=new char[length1+1];
char *s2=new char[length2+1];
for(int i=1;i<=length1;i++){
cin>>s1[i];
}
for(int i=1;i<=length2;i++){
cin>>s2[i];
}
cout<<LCS(s1,length1,s2,length2)<<endl;
delete s1;
delete s2;
system("pause");
return 0;
}
结果也是为3.
2.复杂
若想输出最长公共子序列
#include <iostream>
using namespace std;
int LCS(char *s1,int length1,char *s2,int length2,int **b){
int lcs[50][50]={0};
/************************************************************************/
/* 此时若理解为数组从1开始,0不用,就不会产生诸多的问题
/************************************************************************/
for(int i=1;i<=length1;i++){
for(int j=1;j<=length2;j++){
if(s1[i]==s2[j]){
lcs[i][j]=lcs[i-1][j-1]+1;
b[i][j]=1;
}else if(lcs[i-1][j]>lcs[i][j-1]){
lcs[i][j]=lcs[i-1][j];
b[i][j]=2;
}else{
lcs[i][j]=lcs[i][j-1];
b[i][j]=3;
}
}
}
return lcs[length1][length2];
}
void printLCS(int **b,char *s1,int i,int j){
if (i==0||j==0)
return;
if(b[i][j]==1){
printLCS(b,s1,i-1,j-1);
cout<<s1[i];
}else if(b[i][j]==2){
printLCS(b,s1,i-1,j);
}else{
printLCS(b,s1,i,j-1);
}
}
int main(){
/*a c d f g
a k d f c*/
int length1,length2;
cin>>length1>>length2;
char *s1=new char[length1+1];
char *s2=new char[length2+1];
for(int i=1;i<=length1;i++){
cin>>s1[i];
}
for(int i=1;i<=length2;i++){
cin>>s2[i];
}
/************************************************************************/
/* 在非主函数里new,再delete发生错误,不知为何,最好再主函数中new,再传值到函数中 */
/************************************************************************/
int **b=new int*[length1+1];
for(int i=0;i<=length1;i++){
b[i]=new int[length2+1];
}
cout<<LCS(s1,length1,s2,length2,b)<<endl;
printLCS(b,s1,length1,length2);
/************************************************************************/
/* delete二维数组 */
/************************************************************************/
for(int i=0;i<=length1;i++){
delete []b[i];
}
delete []b;
delete s1;
delete s2;
system("pause");
return 0;
}
但是这只能输出一组LCS,因此需要在上满的printLCS()函数中用一个数组记录结果,是所有的结果都输出。
具体代码以后再写