一、相关概念
1 子序列:如果序列Z={z1,z2,z3…zk},可以由序列X={x1,x2,x3…xn}删除(或不删除)一些元素得到,则Z为X的一个子序列。Z维持了X序列的相对顺序。
2 动态规划:使用分治法将问题划分成一个个子问题,当分解的问题共享子问题时,可采用动态规划。由于分治法在解子问题时存在重复计算,而动态规划只计算一次每个子问题并将其解保存到一个表中,从而避免重复计算。
二、问题描述
求序列Xn={x1,x2,x3…xn} 和序列Ym={y1,y2,y3…ym}的最长公共子序列
三、解决方案
1、思路:
设最长公共子序列为Z={z1,z2,…zk}
如果xn=ym,则xn(ym)一定在Z中,则继续在{x1,x2,…xn-1}(记为Xn-1,后面表示均采用这种记法)和Ym-1中继续寻找最长公共子序列;
若 xn!=ym,则寻找max(Xn-1和Ym的最长公共子序列,Xn与Ym-1的最长公共子序列)
2、伪代码
(1) 采用递归实现
int lcs(X[],Y[])
{
n<-X.length
m<-Y.length
if(n=0||m=0)
return 0
if(X[n-1]=Y[m-1])
{
return lcs(Xn-1,Ym-1)+1
print(X[n-1])
}
else
return max(lcs(Xn-1,Y),lcs(Xn,Ym-1))
}
if(x[n-1]=Y[m-1])
print(X[n-1])
由于递归在运算的过程中存在很多重复子问题,所以一般使用迭代实现动态规划
(2)迭代实现
借助辅助数组B[n][m] , C[n][m] (C[i][j]表示Xi与Yj的lcs的长度)
求最终解时,从B[n][m] 逆序遍历到下标i或j有一个是0,打印出B[i][j]="b"的x[i] 这样就求得X和Y的最长子序列的逆序,然后做些调整,或者使用递归直接打印出正序输出的最长子序列或使用辅助数组记录最长子序列的逆序,然后输出正序也可以
关键过程的伪代码如下:
lcs(X[],Y[])
{
n<-X.length
m<-Y.length
for(i=0;i<n;i++)
C[i][0]=0 //Y字符串长度为0的情况
for(i=0;i<m;i++)
C[0][j] =0 //X字符串长度为0的情况
for(i=1;i<n;i++)
{
for(j=1;j<m;j++)
{
if(X[i]=Y[j])
{
C[i][j]=C[i-1][j-1]+1
B[i][j]=“b”
}
else
if(C[i-1][j-1]>C[i][j-1])
{
C[i][j]=C[i-1][j] //由Xi-1和Yj确定
B[i][j]=“c”
}
else
{
C[i][j]=C[i][j-1] //由Xi和Yj-1确定
B[i][j]=“a”
}
}
}
return B&C
}
上面伪代码得到了B和C,接着只要逆序遍历B,输出值为“b”对应的下标i,j的X[i],即可,不过这样输出的序列是最长子序列的逆序。
伪代码如下:
上面输出的时间复杂度为O(m+n)
如果使用递归则伪代码:
print(B[][],X[],i,j)
{
if(B[i][j]=“b”)
{
return print(B,X,i-1,j-1)
printf(X[i])
}
else if(B[i][j]=“a”)
{return print(B,X,i,j-1)}
else if(B[i][j]=“c”)
{return print(B,X,i-1,j)}
}
综上使用迭代实现可以看出时间复杂度为O(mn),空间复杂度为O(mn)