【重征算法路】之六:最长公共子序列算法 (开始进行动态规划的学习)
5天没看算法了,赶紧把自己拉回来学习、写博客!
解释:分治法、动态规划的区别
1,分治法的基本思想
就拿典型的分治法思想的斐波那契数列来举例,代码如下:
int fibonacci(int n){
if(n<=1) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
斐波那契数列大概的运行导图是这样的:
即:当分治法解决问题时,一般会采用递归的方式,把次问题分解成两个子问题,然后每个子问题再往下分,一直分到该子问题不可再分为止。
2,动态规划的基本思想
我们继续拿上面的斐波那契数列举例。
在分治法计算fibonacci(5)的时候,我们发现:fibonacci(3)计算了2次,fibonacci(2)计算3次,fibonacci(1)计算5次,fibonacci(0)计算3次。
假设每次计算都要消耗一定时间的话,那么重复的计算使得分治法在执行效率上并不高。
如果以动态规划的思想来解斐波那契数列,那么首先会计算fibonacci(0)—fibonacci(5)的值,然后存入表中,然后计算每个fibonacci(n)会用到多少次,最后相加,当调用fibonacci(n)的时候,动态规划的思想是去查表(以往存入值的数组),而不是再次计算。
3,分治法与动态规划的区别
区别在于: 分治法会造成大量的重复计算,而动态规划会把数据存入表中,每次需要计算的时候直接去查表,这样会节省资源空间、提高算法效率。
限制在于 比如斐波那契数列,fibonacci(5)这样的数据的值,是需要一步步推导的,并不能直接算出来,所以这种问题其实还是分治法解决比较简单。
算法介绍
1,算法的问题描述:
若序列X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A},则序列{A,B,C}是序列X的一个子序列,序列{B,C,B,A}是序列X,Y的最长公共子序列。现有X={x1,x2,x3 …… xm},Y={y1,y2,y3 …… yn},找出X,Y的最长公共子序列Z={z1,z2,z3 …… zk}。
2,采用动态规划的算法需要满足两个条件:最优子结构性质、子问题的递归结构
最长公共子序列的最优子结构性质:
(1)若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列。
(2)若xm!=yn且zk!=xm,则Z是Xm-1和Y的最长公共子序列。
(3)若xm!=yn且zk!=yn,则Z是X和Yn-1的最长公共子序列。
子问题的递归结构:
用二维数组c[i][j]来记录序列Xi和Yi的最长公共子序列的长度。
当i=0或j=0时,空序列是Xi或Yi的最长序列,所以此时c[i][j]=0;
其他情况下,由最优子结构性质得递归关系如下:
算法时间复杂度
该算法有两个函数LCSLength和LCS,LCSLength的时间复杂度为O(mn),LCS的时间复杂度为O(m+n)。
算法代码·C++版
void LCSLength(int m,int n,char *x,char *y,int c[][10],int b[][10]){
int i,j;
for(i=1;i<=m;i++) c[i][0]=0;
for(i=1;i<=n;i++) c[0][i]=0;
for(i=1;i<=m;i++)
for(j=1;j<=n;j++){
if(x[i]==y[j])
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if(c[i-1][j]>c[i][j-1])
{
c[i][j]=c[i-1][j];
b[i][j]=2;
}
else
{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
}
void LCS(int i,int j,char *x,int b[][10]){
if(i==0||j==0) return;
if(b[i][j]==1) {LCS(i-1,j-1,x,b);cout<<x[i];}
else if(b[i][j]==2) LCS(i-1,j,x,b);
else LCS(i,j-1,x,b);
}
算法解释
API解释:
该算法的API为:LCSLength(int m,int n,char x,char y,int c,int b)
m 是X字符数组中字符的长度
n 是Y字符数组中字符的长度
x 是X字符数组
y 是Y字符数组
c 是存储Xi和Yi的最长公共子序列的长度的二维数组
b 是记录c中的值是由哪一个子问题的解得到的二维数组
该API无返回值,只是会把二维数组c、b存入数据
该算法的第二API为:LCSLCS(int i,int j,char x,int b)
i 是X字符数组中字符的长度
j 是Y字符数组中字符的长度
x 是X字符数组
b 是记录c中的值是由哪一个子问题的解得到的二维数组
该 API无返回值,但在运行期间会依次输出最长公共子序列
【注:】 字符数组X、Y存放的时候,应该从X[1]开始存放。例如x[8]={’ ',‘a’,‘b’,‘c’,‘d’,‘b’,‘a’,‘c’},其m值为7。Y也同样。
运行解释:
首先,main函数为
int main() {
int m=7,n=6;
int c[10][10]={0},b[10][10]={0};
char x[8]={' ','a','b','c','b','d','a','b'};
char y[7]={' ','b','d','c','a','b','a'};
LCSLength(m,n,x,y,c,b);
LCS(m,n,x,b);
return 0;
}
当运行LCSLength(m,n,x,y,c,b)时,程序运行:
for(i=1;i<=m;i++) c[i][0]=0;
for(i=1;i<=n;i++) c[0][i]=0;
1,以上语句是为含空的字符串赋0;c[i][j]是存储Xi与Yi的最长公共子序列长度,所以当i或者j为0,即X或Y有一方为空的话,最长公共子序列都为0。如下图:
for(i=1;i<=m;i++)
for(j=1;j<=n;j++){
if(x[i]==y[j])
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if(c[i-1][j]>c[i][j-1])
{
c[i][j]=c[i-1][j];
b[i][j]=2;
}
else
{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
2,以上语句是,先取X字符数组中的第一位与Y中的所有依次比较。然后是X中的第二位、第三位……
c[i][j]数组运行大概是这样:(请对照着代码草稿纸上写一遍,很简单的)
3,同理,得出b数组为:
4,最后LCS算法进行对字符串的输出。LCS算法采用递归,虽然是i,j从大到小寻找值,但是因为递归LCS()在cout前面,所以会倒序输出。