最长公共子串(Longest Common Substring ,简称LCS)问题,是指求给定的一组字符串长度最大的共有的子串的问题。例如字符串"abcb","bca","acbc"的LCS就是"bc"。
求多串的LCS,显然穷举法是极端低效的算法。可以用动态规划算法求解。
记Xm={x1,…xm}和Yn={y1,…,yn}为两个字符串,而Zk={z1,…zk}是它们的最长公共子串,则:
1) 如果xm=yn,那么zk=xm=yn,并且Zk-1是Xm-1和Yn-1的一个最长公共子串;
2)如果xm≠yn,那么当zk为空;
有了上面的性质,我们可以得出如下的思路:求两字符串Xm={ x1,…xm}和Yn={y1,…,yn}的LCS,如果xm=yn,那么只需求得Xm-1和Yn-1的最长公共子串,并在其后添加xm(yn)即可;如果xm-1≠yn-1,那么他们的最长公共子串为0。
记字符串Xi和Yj的一个最长公共子串的长度为c[i,j]。如果i=0或j=0,其中一个的长度为0,因此最长公共子串的长度为0。由最长公共子串问题的最优子结构可得递归式:
/ 0 if i=0 or j=0
c[i,j]= c[i-1,j-1]+1 if i,j>0 and xi=yj
\ 0 if i,j>0 and xi≠yj
3 按自底向上方式计算最优解的值
上面的公式用递归函数不难求得。但直接递归(自顶向下)会有很多重复计算,我们用从底向上循环求解的思路效率更高。
为了能够采用循环求解的思路,求取c[i,j]可以从c[i-1,j-1] 这个方向计算得到,相当于在矩阵c[0...m,0...n]中是从c[i-1,j-1]这个方向移动到c[i,j],因此在矩阵中c[i,j]回溯到c[i-1,j-1]的方向是左上方,因为c中值不为0的地方的回溯只有这一个方向,所以不需要用另外一个矩阵b[1..m,1..n]来保存移动的方向。以字符串X=“BABA”和Y=“ABAB”为例,说明这个过程:
第一步初始化c[0,j]和c[i,0]
根据递归公式给c数组赋值如下
根据每一步做出选择的过程(图中箭头)和c中最大值,可以回溯构造出最优解,由此得到的最大公共子串为“BAB”和"ABA"
伪代码如下:
LongestCommonSubstring(x,y){
//初始化 i=0 或j=0
for i=0 to n
c[i,0]=0;
for j=0 to m
c[0,j]=0;
for i=1 to m{
for j=1 to n{
if(x[i]=y[j])
c[i,j]=c[i-1,j-1]+1;
else
c[i,j]=0;
}//end for
}//end for
}//end LongestCommonSubstring()
算法时间复杂度为O(mn)。
由返回的表b和最大值可以用来快速构造Xm={ x1,…xm}和Yn={y1,…,yn}的一个最长公共子串。首先在c中找到最大值的位置,然后从这个位置处开始,沿着当前箭头指向在表格中找到下一个位置,由此跟踪下去。
/**
*i,j为最大值在c中的位置
*/
Print-LCS(b,X,i,j)
//base case
if i=0 或 j=0
return
else
then Print-LCS(b,X,i-1,j-1)