【100题】第五十六题 最长公共子序列 【动态规划精华】
一,题目
如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。
注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。
请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。
例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,则输出它们的长度4,并打印任意一个子串。
二,分析
求最长公共子串(Longest Common Subsequence, LCS)是一道非常经典的动态规划题。
LCS问题的性质:记Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}为两个字符串,而Zk={z0,z1,…zk-1}是它们的LCS,则:
1.
如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
2. 如果xm-1≠yn-1,那么当zk-1≠xm-1时
Z是Xm-1和Y的LCS;
3. 如果xm-1≠yn-1,那么当zk-1≠yn-1时
Z是Yn-1和X的LCS;
下面简单证明一下这些性质:
1. 如果zk-1≠xm-1,那么我们可以把xm-1(yn-1)加到Z中得到Z’,这样就得到X和Y的一个长度为k+1的公共子串Z’。这就与长度为k的Z是X和Y的LCS相矛盾了。因此一定有zk-1=xm-1=yn-1。
既然zk-1=xm-1=yn-1,那如果我们删除zk-1(xm-1、yn-1)得到的Zk-1,Xm-1和Yn-1,显然Zk-1是Xm-1和Yn-1的一个公共子串,现在我们证明Zk-1是Xm-1和Yn-1的LCS。用反证法不难证明。假设有Xm-1和Yn-1有一个长度超过k-1的公共子串W,那么我们把加到W中得到W’,那W’就是X和Y的公共子串,并且长度超过k,这就和已知条件相矛盾了。
2. 还是用反证法证明。假设Z不是Xm-1和Y的LCS,则存在一个长度超过k的W是Xm-1和Y的LCS,那W肯定也X和Y的公共子串,而已知条件中X和Y的公共子串的最大长度为k。矛盾。
3. 证明同2。
有了上面的性质,我们可以得出如下的思路:
求两字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,
如果xm-1=yn-1,那么只需求得Xm-1和Yn-1的LCS,并在其后添加xm-1(yn-1)即可;
如果xm-1≠yn-1,我们分别求得Xm-1和Y的LCS和Yn-1和X的LCS,并且这两个LCS中较长的一个为X和Y的LCS。
如果我们记字符串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
上面的公式用递归函数不难求得。但从前面求Fibonacci第n项(本微软等100题系列第19题)的分析中我们知道直接递归会有很多重复计算,我们用从底向上循环求解的思路效率更高。
为了能够采用循环求解的思路,我们用一个矩阵(参考代码中的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)保存移动的方向。
三,代码中采用两种方法
#include "stdio.h" int c[10][10];//说明X有7个元素 Y有六个元素 (保存的是最长子序列的个数) char b[8][7];//记录将要如何输出 最长子序列 void lcs_length(char X[],char Y[],int m,int n) { for(int i=1;i<=m;i++)//初始化 Y没有元素的时候 竖着的一列 c[i][0]=0; for(int j=1;j<=n;j++) c[0][j]=0; for(int k=1;k<=m;k++) { for(int l=1;l<=n;l++) { if(X[k]==Y[l]) { c[k][l]=c[k-1][l-1]+1; b[k][l]='!';//左上走 } else if(c[k-1][l]>=c[k][l-1]) { c[k][l]=c[k-1][l]; b[k][l]='@';//向上走 } else { c[k][l]=c[k][l-1]; b[k][l]='#';//向左走 } } } } void print_lcs(char b[][7],char X[],int i,int j) { if(i==0||j==0) return; if(b[i][j]=='!')//左上找 { print_lcs(b,X,i-1,j-1); printf("%c",X[i]); } else if(b[i][j]=='@') //向上找 print_lcs(b,X,i-1,j); else print_lcs(b,X,i,j-1); } void no_B_print(char X[],char Y[],int m,int n) //不用标记 只用C 输出 { int i=m;//7 int j=n;//6 char a[m]; int k=0; while(i>0&&j>0) { if(X[i]==Y[j]) { a[k++]=X[i]; //printf("%c",X[i]);//一定要写成 %C 不要写成 %S i--; j--; } else if(c[i-1][j]>=c[i][j-1])//上侧大 i--; else j--; } for(int l=k;l>=0;l--)//正序输出 printf("%c",a[l]); } int lookup(char X[],char Y[],int i,int j) { if(c[i][j]>-1) return c[i][j]; if(i==0||j==0) c[i][j]=0; else { if(X[i]==Y[j]) c[i][j]=lookup(X,Y,i-1,j-1)+1; else { if(lookup(X,Y,i-1,j)>=lookup(X,Y,i,j-1)) c[i][j]=lookup(X,Y,i-1,j); else c[i][j]=lookup(X,Y,i,j-1); } } return c[i][j]; } void memor_Lcs(char X[],char Y[],int m,int n) { for(int i=1;i<=m;i++)//标记一个数(不会通过后来的计算得到的数) 方便后来比较 看是否计算过 for(int j=1;j<=n;j++) c[i][j]=-1; lookup(X,Y,m,n); } int main() { char X[]={'@','A','B','C','B','D','A','B'}; char Y[]={'@','B','D','C','A','B','A'}; //lcs_length(X,Y,7,6); memor_Lcs(X,Y,7,6);//备忘录 //print_lcs(b, X,7,6);//利用 做标记 的b标记数组 输出 no_B_print(X,Y,7,6);//只利用 c输出 printf("\n"); for(int i=0;i<8;i++) { for(int j=0;j<7;j++) printf("%2d",c[i][j]); ///特殊符号的选取错误 ? printf("\n"); } //printf("%C\n",c[2][1]); ///特殊符号的选取错误 ? return 0; }
扩展:如果题目改成求两个字符串的最长公共子字符串,应该怎么求?子字符串的定义和子串的定义类似,但要求是连续分布在其他字符串中。比如输入两个字符串BDCABA和ABCBDAB的最长公共字符串有BD和AB,它们的长度都是2。
参考针对此题写的一篇博文:24个经典算法系列:3、动态规划算法解一道面试题
http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6110269.aspx