动态规划算法文档格式.docx
《动态规划算法文档格式.docx》由会员分享,可在线阅读,更多相关《动态规划算法文档格式.docx(8页珍藏版)》请在冰豆网上搜索。
56.最长公共子序列。
题目:
如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,
则字符串一称之为字符串二的子串。
注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。
请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。
例如:
输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,
则输出它们的长度4,并打印任意一个子串。
分析:
求最长公共子串(LongestCommonSubsequence,LCS)是一道非常经典的动态规划题,
因此一些重视算法的公司像MicroStrategy都把它当作面试题。
步骤一、描述一个最长公共子序列
先介绍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;
下面简单证明一下由上述相应条件得出的这些性质:
如果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,这就和已知条件相矛盾了。
还是用反证法证明。
假设Z不是Xm-1和Y的LCS,则存在一个长度超过k的W是Xm-1和Y的LCS,那W肯定也X和Y的公共子串,而已知条件中X和Y的公共子串的最大长度为k。
矛盾。
证明同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
ifi<
0orj<
c[i,j]=
c[i-1,j-1]+1
ifi,j>
=0andxi=xj
max(c[i,j-1],c[i-1,j]
=0andxi≠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)保存移动的方向。
步骤三,计算LCS的长度
LCS-LENGTH(X,Y)
1m←length[X]
2n←length[Y]
3fori←1tom
4
doc[i,0]←0
5forj←0ton
6
doc[0,j]←0
7fori←1tom
8
doforj←1ton
9
doifxi=yj
10
thenc[i,j]←c[i-1,j-1]+1
11
b[i,j]←"
↖"
12
elseifc[i-1,j]≥c[i,j-1]
13
thenc[i,j]←c[i-1,j]
14
↑"
15
elsec[i,j]←c[i,j-1]
16
b[i,j]←←
17returncandb
此过程LCS-LENGTH以俩个序列X=〈x1,x2,...,xm〉和Y=〈y1,y2,...,yn〉为输入。
它把c[i,j]值填入一个按行计算表项的表c[0‥m,0‥n]中,它还维护b[1‥m,1‥n]以简化最优解的构造。
从直觉上看,b[i,j]指向一个表项,这个表项对应于与在计算c[i,j]时所选择的最优子问题的解是相同的。
该程序返回表中bandc,c[m,n]包含X和Y的一个LCS的长度。
步骤四,构造一个LCS,
PRINT-LCS(b,X,i,j)
1ifi=0orj=0
2
thenreturn
3ifb[i,j]="
thenPRINT-LCS(b,X,i-1,j-1)
5
printxi
6elseifb[i,j]="
7
thenPRINT-LCS(b,X,i-1,j)
8elsePRINT-LCS(b,X,i,j-1)
该过程的运行时间为O(m+n)。
-------------------------------
ok,最后给出此面试第56题的代码,请君自看:
参考代码如下:
#include"
string.h"
//directionsofLCSgeneration
enumdecreaseDir{kInit=0,kLeft,kUp,kLeftUp};
//Getthelengthoftwostrings'
LCSs,andprintoneoftheLCSs
//Input:
pStr1
-thefirststring
//
pStr2
-thesecondstring
//Output:
thelengthoftwostrings'
LCSs
intLCS(char*pStr1,char*pStr2)
{
if(!
pStr1||!
pStr2)
return0;
size_tlength1=strlen(pStr1);
size_tlength2=strlen(pStr2);
length1||!
length2)
size_ti,j;
//initiatethelengthmatrix
int**LCS_length;
LCS_length=(int**)(newint[length1]);
for(i=0;
i<
length1;
++i)
LCS_length[i]=(int*)newint[length2];
for(j=0;
j<
length2;
++j)
LCS_length[i][j]=0;
//initiatethedirectionmatrix
int**LCS_direction;
LCS_direction=(int**)(newint[length1]);
for(i=0;
LCS_direction[i]=(int*)newint[length2];
LCS_direction[i][j]=kInit;
{
if(i==0||j==0)
if(pStr1[i]==pStr2[j])
LCS_length[i][j]=1;
LCS_direction[i][j]=kLeftUp;
}
else
//acharofLCSisfound,
//itcomesfromtheleftupentryinthedirectionmatrix
elseif(pStr1[i]==pStr2[j])
LCS_length[i][j]=LCS_length[i-1][j-1]+1;
//itcomesfromtheupentryinthedirectionmatrix
elseif(LCS_length[i-1][j]>
LCS_length[i][j-1])
LCS_length[i][j]=LCS_length[i-1][j];
LCS_direction[i][j]=kUp;
//itcomesfromtheleftentryinthedirectionmatrix
LCS_length[i][j]=LCS_length[i][j-1];
LCS_direction[i][j]=kLeft;
LCS_Print(LCS_direction,pStr1,pStr2,length1-1,length2-1);
//调用下面的LCS_Pring打印出所求子串。
returnLCS_length[length1-1][length2-1];
//返回长度。
}
//PrintaLCSfortwostrings
LCS_direction-a2dmatrixwhichrecordsthedirectionof
LCSgeneration
row
-therowindexinthematrixLCS_direction
col
-thecolumnindexinthematrixLCS_direction
voidLCS_Print(int**LCS_direction,
char*pStr1,char*pStr2,
size_trow,size_tcol)
if(pStr1==NULL||pStr2==NULL)
return;
if(length1==0||length2==0||!
(row<
length1&
&
col<
length2))
//kLeftUpimpliesacharintheLCSisfound
if(LCS_direction[row][col]==kLeftUp)
if(row>
0&
col>
0)
LCS_Print(LCS_direction,pStr1,pStr2,row-1,col-1);
//printthechar
printf("
%c"
pStr1[row]);
elseif(LCS_direction[row][col]==kLeft)
//movetotheleftentryinthedirectionmatrix
if(col>
LCS_Print(LCS_direction,pStr1,pStr2,row,col-1);
elseif(LCS_direction[row][col]==kUp)
//movetotheupentryinthedirectionmatrix
LCS_Print(LCS_direction,pStr1,pStr2,row-1,col);
扩展:
如果题目改成求两个字符串的最长公共子字符串,应该怎么求?
子字符串的定义和子串的定义类似,但要求是连续分布在其他字符串中。
比如输入两个字符串BDCABA和ABCBDAB的最长公共字符串有BD和AB,它们的长度都是2。
附注:
算法导论上指出,
一、最长公共子序列问题的一个一般的算法、时间复杂度为O(mn)。
然后,Masek和Paterson给出了一个O(mn/lgn)时间内执行的算法,其中n<
=m,而且此序列是从一个有限集合中而来。
在输入序列中没有出现超过一次的特殊情况中,Szymansk说明这个问题可在O((n+m)lg(n+m))内解决。
二、一篇由Gilbert和Moore撰写的关于可变长度二元编码的早期论文中有这样的应用:
在所有的概率pi都是0的情况下构造最优二叉查找树,这篇论文给出一个O(n^3)时间的算法。
Hu和Tucker设计了一个算法,它在所有的概率pi都是0的情况下,使用O(n)的时间和O(n)的空间,
最后,Knuth把时间降到了O(nlgn)。
关于此动态规划算法更多可参考算法导论一书第15章动态规划问题,