最长子串(动态规划)
题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,
则字符串一称之为字符串二的子串。
注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。
请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。
例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,
则输出它们的长度4,并打印任意一个子串。
分析:求最长公共子串(Longest CommonSubsequence, LCS)是一道非常经典的动态规划题。
以下分析参见另外的一篇博文。
步骤一、描述一个最长公共子序列
先介绍LCS问题的性质:记Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}为两个字符串,
并设Zk={z0,z1,…zk-1}是X和Y的任意一个LCS,则可得出3条性质:
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是X和Yn-1的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)即可(上述性质1);
如果xm-1≠yn-1,我们分别求得Xm-1和Y的LCS和Yn-1和X的LCS,并且这两个LCS中较长的一个为X和Y的LCS(上述性质2、3)。
根据上述结论,可得到以下公式,
如果我们记字符串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题系列V0.1版第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)保存移动的方向。
然后下面也是参见其博文后,修改部分所得到的C++实现源码:
// 动态规划_最大子串.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include<string> #include<iostream> using namespace std; enum decreaseDir{kInit=0,kLeft,kUp,kLeftUp}; void LCS_Print(int** LCS_dirction,string pStr1,string pStr2,int row,int col); int LCS(string pStr1,string pStr2) { //if(!pStr1||!pStr2)return 0; int length1=pStr1.length(); int length2=pStr2.length(); if(!length1||!length2)return 0; int i,j; int** LCS_length; LCS_length=(int**)(new int[length1]); for(i=0;i<length1;i++) LCS_length[i]=(int*)new int[length2]; for(i=0;i<length1;++i) for(j=0;j<length2;++j) LCS_length[i][j]=0; //初始化length matrix int** LCS_dirction; LCS_dirction=(int**)(new int[length1]); for(i=0;i<length1;++i) LCS_dirction[i]=(int*)new int[length2]; for(i=0;i<length1;++i) for(j=0;j<length2;++j) LCS_dirction[i][j]=kInit; //初始化dirction matrix for(i=0;i<length1;++i) { for(j=0;j<length2;++j) { if(i==0||j==0) { if(pStr1[i]==pStr2[j]) { LCS_length[i][j]=1; LCS_dirction[i][j]=kLeftUp; } else LCS_length[i][j]=0; } else if(pStr1[i]==pStr2[j]) { LCS_length[i][j]=LCS_length[i-1][j-1]+1; LCS_dirction[i][j]=kLeftUp; } else if(LCS_length[i-1][j]>LCS_length[i][j-1]) { LCS_length[i][j]=LCS_length[i-1][j]; LCS_dirction[i][j]=kUp; } else { LCS_length[i][j]=LCS_length[i][j-1]; LCS_dirction[i][j]=kLeft; } } } LCS_Print(LCS_dirction,pStr1,pStr2,length1-1,length2-1); return LCS_length[length1-1][length2-1]; } void LCS_Print(int** LCS_dirction,string pStr1,string pStr2,int row,int col) { //if(pStr1==NULL||pStr2==NULL)return; int length1=pStr1.length(); int length2=pStr2.length(); if(length1==0||length2==0||!(row<length1&&col<length2))return; if(LCS_dirction[row][col]==kLeftUp) { if(row>0&&col>0) LCS_Print(LCS_dirction,pStr1,pStr2,row-1,col-1); printf("%c",pStr1[row]); } else if(LCS_dirction[row][col]==kLeft) { if(col>0) LCS_Print(LCS_dirction,pStr1,pStr2,row,col-1); } else if(LCS_dirction[row][col]==kUp) { if(row>0) LCS_Print(LCS_dirction,pStr1,pStr2,row-1,col); } } int _tmain(int argc, _TCHAR* argv[]) { string str1="BDCABA"; //char str1[]={'B','D','C','A','B','A'}; string str2="ABCBDAB"; //char str2[]={'A','B','C','B','D','A','B'}; cout<<"存在的一个最大子串为:"<<endl; int Length=LCS(str1,str2); cout<<endl<<"最大子串的长度为:"<<Length<<endl; int k=0; cin>>k; return 0; }
程序的运行截图: