题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。
例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,则输出它们的长度4,并打印任意一个子串。
分析:求最长公共子串(Longest Common Subsequence, LCS)是一道非常经典的动态规划题,因此一些重视算法的公司像MicroStrategy都把它当作面试题。
完整介绍动态规划将需要很长的篇幅,因此我不打算在此全面讨论动态规划相关的概念,只集中对LCS直接相关内容作讨论。如果对动态规划不是很熟悉,请参考相关算法书比如算法讨论。
考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bn-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果am-1==bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;
(2) 如果am-1!=bn-1,则若zk-1!=am-1时,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;
(3) 如果am-1!=bn-1,则若zk-1!=bn-1时,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。
这样,在找A和B的公共子序列时,如果有am-1==bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。
求解:
引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定输出最长公共字串时搜索的方向。
我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] == Y[j]还是X[i] != Y[j],就可以计算出c[i][j]。
问题的递归式写成:
回溯输出最长公共子序列过程:
算法分析(打印也就是回溯时):
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为O(m + n)。
#include <stdio.h>
#include <string.h>
int LCS_len(char a[], char b[], int len_a, int len_b, int **d)
{
if(a==NULL || b==NULL || len_a<=0 || len_b<=0 || d==NULL)
return 0;
int i,j;
int **c = new int*[len_a+1];//c[i][j]存储a[0...i-1]和b[0...j-1]的最大公共子串长度
for(i=0;i<len_a+1;i++)
c[i] = new int[len_b+1];
for(i=0;i<len_a+1;i++)//第一列清0
c[i][0] = 0;
for(i=0;i<len_b+1;i++)//第一行清0
c[0][i] = 0;
for(i=1;i<len_a+1;i++)
{
for(j=1;j<len_b+1;j++)
{
if(a[i-1]==b[j-1])//注意这里是i-1和j-1,因为c数组是从1开始的,为了防止数组越界
{
c[i][j] = c[i-1][j-1]+1;
d[i][j] = 0;//右下方,看下面main()中的解析
}
else if(c[i-1][j] > c[i][j-1])
{
c[i][j] = c[i-1][j];
d[i][j] = 1;//正下方
}
else
{
c[i][j] = c[i][j-1];
d[i][j] = -1;//正右方
}
}
}
int count = c[len_a][len_b];//最大公共子串的长度是c数组的最后一个值
for(i=0;i<len_a+1;i++)
delete [] c[i];
delete [] c;
c = NULL;
return count;
}
void print_str_core(char a[], int i, int j, int **d)//回溯打印,从最后开始递归
{
if(i==0 || j==0)
return;
if(d[i][j]==0)//回溯方向是原方向的反方向,此时是左上方
{
print_str_core(a,i-1,j-1,d);//向左上方递归
printf("%c",a[i-1]);//注意这里是i-1,只有在此时最大公共子串长度才会变化,打印
}
else if(d[i][j]==1)
{
print_str_core(a,i-1,j,d);//向上方递归
}
else if(d[i][j]==-1)
{
print_str_core(a,i,j-1,d);//向左方递归
}
}
void print_str(char a[], int len_a, int len_b, int **d)
{
if(a==NULL || len_a<=0 || len_b<=0 || d==NULL)
return;
print_str_core(a,len_a,len_b,d);
}
int main()
{
char a[20],b[20];
printf("请输入字符串1:");
scanf("%s",a);
printf("请输入字符串2:");
scanf("%s",b);
int len_a = strlen(a);
int len_b = strlen(b);
int **d = new int*[len_a+1];//d二维数组用于记录得到c[i][j]是的方向,得到c[i][j]有三条路径c[i-1][j-1]、c[i-1][j]和c[i-1][j]
//其中d数组中如果值为0,代表c[i][j]是从c[i-1][j-1]得到的,也就是方向是右下方
//其中d数组中如果值为1,代表c[i][j]是从c[i-1][j]得到的,也就是方向是正下方
//其中d数组中如果值为-1,代表c[i][j]是从c[i][j-1]得到的,也就是方向是正右方
int i;
for(i=0;i<len_a+1;i++)
d[i] = new int[len_b+1];
int count = LCS_len(a,b,len_a,len_b,d);
printf("最长公共子串的长度为:%d\n",count);
printf("最长公共子串为:");
print_str(a,len_a,len_b,d);
printf("\n");
for(i=0;i<len_a+1;i++)
delete [] d[i];
delete [] d;
d=NULL;
return 0;
}