题目:最长公共子串
题目描述
信息学小组截获了两个序列,序列A和B,规定两个序列所隐藏的信息就是两者的最长公共子串(注意,这里的子串是指连续的,比如说ABCDEFG中ABC是ABCDEFG的子串,而ABD或者ABE都不是ABCDEFG的子串),现在,他们将这个任务交给你,你要找出这两个序列所隐藏的信息
输入格式
两行,A和B(AB长度均不大于1000,AB均由大写字母组成,且AB长度相等)
输出格式
一个序列,表示A和B的最长公共子串(若有多个最长子串,则输出最先出现的一个,数据保证存在解)
题解:———————————————————————————————————————————————————
最长公共子串(LCS),有三种情况:1.公共子串的元素必须相邻. 2.公共子串的元素可以不相邻联单3. 求多个字符串而不是两个字符串的最长公共子串
1.公共子串的元素必须相邻:
LCS问题就是求两个字符串最长公共子串的问题。解法就是用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。然后求出对角线最长的1序列,其对应的位置就是最长匹配子串的位置.
下面是字符串21232523311324和字符串312123223445的匹配矩阵,前者为X方向的,后者为Y方向的。不难找到,红色部分是最长的匹配子串。通过查找位置我们得到最长的匹配子串为:21232
0 0 0 1 0 0 0 1 1 0 0 1 0 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 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,而是赋上其左上角元素的值加一。我们用两个标记变量来标记矩阵中值最大的元素的位置,在矩阵生成的过程中来判断当前生成的元素的值是不是最大的,据此来改变标记变量的值,那么到矩阵完成的时候,最长匹配子串的位置和长度就已经出来了。
算法的基本思想:
当字符匹配的时候,不是简单的给相应元素赋上1,而是赋上其左上角元素的值加一。
我们用两个标记变量来标记矩阵中值最大的元素的位置,在矩阵生成的过程中来判断
当前生成的元素的值是不是最大的,据此来改变标记变量的值,那么到矩阵完成的时
候,最长匹配子串的位置和长度就已经出来了。
===========================================================================
程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#include<string.h> #define M 100 //LCS问题就是求两个字符串最长公共子串的问题 char * LCS( char left[], char right[]) { //获取左子串的长度,获取右子串的长度 int lenLeft= strlen (left),lenRight= strlen (right),k; //注意这里要写成char型,而不是int型,否则输入整型数据时会产生错误。 //矩阵c纪录两串的匹配情况 char *c= malloc (lenRight),*p; //int c[M][M]={0};//当将c申明为一个二维数组时 int start,end,len,i,j; //start表明最长公共子串的起始点,end表明最长公共子串的终止点 end=len=0; //len表示最长公共子串的长度 for (i=0; i<lenLeft; i++) //串1从前向后比较 { //串2从后向前比较,为什么要从后向前呢?是把一维数组c[ ]当二维数组来用, //如果要从前向后,可以将c申明为一个二维数组c[M][M].但程序要做相应调整. // for(j=0;j<lenRight;j++)//当c申明为一个二维数组时 for (j=lenRight-1; j>=0; j--) { if (left[i] == right[j]) //元素相等时 { if (i==0||j==0) c[j]=1; //c[i][j]=1; else { c[j]=c[j-1]+1; //c[i][j]=c[i-1][j-1]+1; } } else c[j] = 0; //c[i][j]=0; if (c[j] > len) //if (c[i][j]>len) { len=c[j]; //len=c[i][j]; end=j; } } } start=end-len+1; //数组p纪录最长公共子串 p =( char *) malloc (len+1); for (i=start; i<=end; i++) { p[i-start] = right[i]; } p[len]= '\0' ; return p; } void main() { char str1[M],str2[M]; printf ( "请输入字符串1:" ); gets (str1) printf ( "请输入字符串2:" ); gets (str2); printf ( "最长子串为:" ); printf ( "%s\n" ,LCS(str1,str2)); } |
==========================================================================
程序测试:
输入
字符串1:21232523311324
字符串2:312123223445
数组c的变化情况为:
0 0 1 0 1 0 1 1 0 0 0 0
0 1 0 2 0 0 0 0 0 0 0 0
0 0 2 0 3 0 1 1 0 0 0 0
1 0 0 0 0 4 0 0 2 0 0 0
0 0 1 0 1 0 5 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1
0 0 1 0 1 0 1 1 0 0 0 0
1 0 0 0 0 2 0 0 2 0 0 0
1 0 0 0 0 1 0 0 1 0 0 0
0 2 0 1 0 0 0 0 0 0 0 0
0 1 0 1 0 0 0 0 0 0 0 0
1 0 0 0 0 1 0 0 1 0 0 0
0 0 1 0 1 0 2 1 0 0 0 0
0 0 0 0 0 0 0 0 0 1 1 0
长:14(串1的长度),宽:12(串2的长度)
最长子串为:21232
评论:该算法只能打印出最长公共子串中的一个,而不是全部解.
2.公共子串的元素可以不相邻
如果我们记字符串Xi和Yj的LCS的长度为c[i,j],我们可以递归地求c[i,j]:
/ 0 if i<0 or j<0
c[i,j]= c[i-1,j-1]+1 if i,j>=0 and xi=xj
\ max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj
上面的公式用递归函数不难求得。我们知道直接递归会有很多重复计算,我们用从底向上循环求解的思路效率更高。
为了能够采用循环求解的思路,我们用一个矩阵(参考代码中的LCS_length)保存下来当前已经计算好了的c[i,j],当后面的计算需要这些数据时就可以直接从矩阵读取。另外,求取c[i,j]可以从c[i-1,j-1] 、c[i,j-1]或者c[i-1,j]三个方向计算得到,相当于在矩阵LCS_length中是从c[i-1,j-1],c[i,j-1]或者c[i-1,j]的某一个各自移动到c[i,j],因此在矩阵中有三种不同的移动方向:向左、向上和向左上方,其中只有向左上方移动时才表明找到LCS中的一个字符。于是我们需要用另外一个矩阵(参考代码中的LCS_direction)保存移动的方向。
动态规划算法可有效地解此问题。下面我们按照动态规划算法设计的各个步骤来设计一个解此问题的有效算法。
1.最长公共子序列的结构
解最长公共子序列问题时最容易想到的算法是穷举搜索法,即对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且在检查过程中选出最长的公共子序列。X的所有子序列都检查过后即可求出X和Y的最长公共子序列。X的一个子序列相应于下标序列{1, 2, …, m}的一个子序列,因此,X共有2m个不同子序列,从而穷举搜索法需要指数时间。
事实上,最长公共子序列问题也有最优子结构性质,因为我们有如下定理:
定理: 最优子结构性质。
2.子问题的递归结构
由最长公共子序列问题的最优子结构性质可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行:当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。
由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。
与矩阵连乘积最优计算次序问题类似,我们来建立子问题的最优值的递归关系。用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。其他情况下,由定理可建立递归关系如下:
3.计算最优值
直接利用(2.2)式容易写出一个计算c[i,j]的递归算法,但其计算时间是随输入长度指数增长的。由于在所考虑的子问题空间中,总共只有θ(m*n)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。
计算最长公共子序列长度的动态规划算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作为输入。输出两个数组c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存储Xi与Yj的最长公共子序列的长度,b[i,j]记录指示c[i,j]的值是由哪一个子问题的解达到的,这在构造最长公共子序列时要用到。最后,X和Y的最长公共子序列的长度记录于c[m,n]中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
#include <cstdlib> #include <iostream> #include "iomanip.h" #define max 100 using namespace std; void LCSLength( int m , int n , char *x , char *y , char b[][max] ) { int i , j , k; int c[max][max]; for ( i = 1 ; i <= m ; i++ ) { c[i][0] = 0; } for ( i = 1 ; i <= n ; i++ ) { c[0][i] = 0; } for ( i = 1 ; i <= m ; i++ ) { for ( j = 1 ; j <= n ; j++ ) { if ( x[i-1] == y[j-1] ) { c[i][j] = c[i-1][j-1] + 1; b[i][j] = '\\' ; } else if ( c[i-1][j] >= c[i][j-1] ) { c[i][j] = c[i-1][j]; b[i][j] = '|' ; } else { c[i][j] = c[i][j-1]; b[i][j] = '-' ; } } //for printf ( " " ); for ( j = 1 ; j <= n ; j++ ) printf ( "%2c" ,b[i][j]); printf ( "\n" ); printf ( "%2c" ,x[i-1]); printf ( "%2d" ,0); for ( j = 1 ; j <= n ; j++ ) printf ( "%2d" ,c[i][j]); printf ( "\n" ); } //for printf ( "\n" ); } void LCS( int i , int j , char *x , char b[][max]) { if ( i == 0 || j == 0 ){ return ;} if ( b[i][j] == '\\' ) { LCS( i - 1 , j - 1 , x , b); cout<<x[i-1]; } else if ( b[i][j] == '|' ) { LCS( i - 1 , j , x , b ); } else { LCS( i , j - 1 , x , b ); } } int main() { char x[max] = { 'e' , 'g' , 'c' , 'r' , 'b' , 'a' , 'd' }; char y[max] = { 'e' , 'd' , 'h' , 'e' , 'b' , 'd' }; int m = 7; int n = 6; char b[max][max] = { 0 }; printf ( " " ); for ( int j = 1 ; j <= n ; j++ ) printf ( "%2c" ,y[j-1]); printf ( "\n" ); printf ( " " ); for ( int k = 1 ; k <= m ; k++ ) printf ( "%2d" ,0); printf ( "\n" ); LCSLength( m , n , x , y , b ); printf ( "最长公共序列串为:" ); LCS( m , n , x , b ); cout<<endl<<endl; system ( "PAUSE" ); return EXIT_SUCCESS; } |
该程序在Dev c++ 4.9.9.2中调试并运行通过.
由于每个数组单元的计算耗费Ο(1)时间,算法LCS_LENGTH耗时Ο(mn)。
4.构造最长公共子序列
由算法LCS_LENGTH计算得到的数组b可用于快速构造序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列。首先从b[m,n]开始,沿着其中的箭头所指的方向在数组b中搜索。当b[i,j]中遇到"↖"时,表示Xi与Yj的最长公共子序列是由Xi-1与Yj-1的最长公共子序列在尾部加上xi得到的子序列;当b[i,j]中遇到"↑"时,表示Xi与Yj的最长公共子序列和Xi-1与Yj的最长公共子序列相同;当b[i,j]中遇到"←"时,表示Xi与Yj的最长公共子序列和Xi与Yj-1的最长公共子序列相同。
下面的算法LCS(i,j,b,x)实现根据b的内容打印出Xi与Yj的最长公共子序列。通过算法的调用LCS(length[X],length[Y],b,X),便可打印出序列X和Y的最长公共子序列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void LCS( int i , int j , char *x , char b[][max]) { if ( i == 0 || j == 0 ){ return ;} if ( b[i][j] == '\\' ) { LCS( i - 1 , j - 1 , x , b); cout<<x[i-1]; } else if ( b[i][j] == '|' ) { LCS( i - 1 , j , x , b ); } else { LCS( i , j - 1 , x , b ); } } |
在算法LCS中,每一次的递归调用使i或j减1,因此算法的计算时间为O(m+n)。
例如,设所给的两个序列为X={'e' , 'g' , 'c' , 'r' , 'b' , 'a' , 'd'}和Y={'e' , 'd' , 'h' , 'e' , 'b' , 'd'}.由算法LCS_LENGTH和LCS计算出的结果如图2所示。
图2 算法LCS的计算结果
5.算法的改进
对于一个具体问题,按照一般的算法设计策略设计出的算法,往往在算法的时间和空间需求上还可以改进。这种改进,通常是利用具体问题的一些特殊性。
例如,在算法LCS_LENGTH和LCS中,可进一步将数组b省去。事实上,数组元素c[i,j]的值仅由c[i-1,j-1],c[i-1,j]和c[i,j-1]三个值之一确定,而数组元素b[i,j]也只是用来指示c[i,j]究竟由哪个值确定。因此,在算法LCS中,我们可以不借助于数组b而借助于数组c本身临时判断c[i,j]的值是由c[i-1,j-1],c[i-1,j]和c[i,j-1]中哪一个数值元素所确定,代价是Ο(1)时间。既然b对于算法LCS不是必要的,那么算法LCS_LENGTH便不必保存它。这一来,可节省θ(mn)的空间,而LCS_LENGTH和LCS所需要的时间分别仍然是Ο(mn)和Ο(m+n)。不过,由于数组c仍需要Ο(mn)的空间,因此这里所作的改进,只是在空间复杂性的常数因子上的改进。
另外,如果只需要计算最长公共子序列的长度,则算法的空间需求还可大大减少。事实上,在计算c[i,j]时,只用到数组c的第i行和第i-1行。因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。更进一步的分析还可将空间需求减至min(m, n)。
求多个字符串的最长公共子串
最长公共子串(Longest common substring, 简称LCS)问题指的是求出给定的一组
字符串的长度最大的共有的子字符串。
举例说明,以下三个字符串的LCS就是 cde:
abcde
cdef
ccde
高效的查找LCS算法可以用于比较多篇文章的最长相同片段,以及生物学上的基因比
较等实际应用。
前几天写了一个穷举法的简单实现,感觉在数据量稍大时效率极低,所以今天上网查
了一些资料,找到了解决LCS问题的最佳算法并编程实现,程序效率得到了极大的提
高。
采用的是广义后缀树(Generalized Suffix Tree,简称GST)算法,就是把给定的N个
源字符串的所有的后缀建成一颗树,这个树有以下一些特点:
1.树的每个节点是一个字符串,树根是空字符串“”
2.任意一个后缀子串都可以由一条从根开始的路径表达
(将这条路径上的节点字符串依次拼接起来就可以得到这个后缀)
3.特别应注意任意一个子串都可以看作某一个后缀的前缀。既然每一个后缀
都可以由一条从根开始的路径表达,那么我们可以从根节点开始一个字符
一个字符的跟踪这条路径从而得到任意一个子串。
4.为了满足查找公共子串的需求,每个节点还应该有从属于哪个源字符串的
信息
由以上的定义我们不难看出,在这棵GST树上,如果找到深度最大并且从属于所有源
字串的节点,那么,把从根到这个节点的路径上的所有节点字符串拼接起来就是
LCS。
还是举例说明,上面提到的三个字符串【abcde cdef ccde】的所有后缀子串列表如
下:
(注:.1表示是从第一个串abcde来的,同理.2,.3分别表示从cdef,ccde来的)
abcde.1
bcde.1
cde.1
de.1
e.1
cdef.2
def.2
ef.2
f.2
ccde.3
cde.3
de.3
e.3
建成的GST如下图所示
(注:.1表示从属于第一个串,.123表示既从属于第一又从属于第二,第三个源串)
--\_______【abcde.1】
|
|_____【bcde.1】 .....最深的并且带.123的节点
| :
|_____【c.123】____【de.123】_______【f.2】
| |
| |__【cde.3】
|
|_____【de.123】___【f.2】
|
|_____【e.123】____【f.2】
|
|_____【f.2】
上图中虚线所指的【de.123】节点所表示的子串cde正是LCS
以上是一些基本概念,但是实际应用时还要涉及到构建GST树以及查找LCS的具体算
法,参考了网上的一些资料,我用java语言实现了这些算法,基本上可以以O(n)的时间
复杂度进行建树及查找处理。
如果对构建SuffixTree算法等细节感兴趣,可以到google上查阅相关资料。
代码实现:
#include<iostream>
using namespace std;
int h[1001][1001]={0};char a[1001],b[1001];
int main()
{
int i,j,max=0,x;
cin>>a>>b;
memset(h,0,sizeof(h));
for(i=1;i<=strlen(a);i++)
for(j=1;j<=strlen(a);j++)
if(a[i-1]==b[j-1]) {h[i][j]=h[i-1][j-1]+1;if(h[i][j]>max) {max=h[i][j];x=i;}}
for(j=x-max;j<x;j++)
cout<<a[j];
cout<<endl;
return 0;
system("pause");
}