最长公共子序列问题
问题描述:给定两个序列 X , Y X,Y X,Y(可以把它们视作字符串),求他们的最长公共子序列。例如 X = " B C A B " , Y = " B D A C B " X="BCAB",Y="BDACB" X="BCAB",Y="BDACB",其最长公共子序列为 B C B , B A B BCB,BAB BCB,BAB。一个给定序列的子序列,就是将给定序列零个或多个元素去掉之后的序列。例如 X = " B A C " X="BAC" X="BAC",它有 B , A , C , B A , B C , A C , B A C B,A,C,BA,BC,AC,BAC B,A,C,BA,BC,AC,BAC,六个子序列。如何利用动态规划求解这个问题?(最长公共子序列问题,Longest common subsequence,LCS)
求解动态规划问题,首先都要找到它是否满足最优子结构特征(最优子结构特征是指最优化问题的最优解可以由子问题的最优解组合得到)。
假设 X = x 1 x 1 x 2 … … x n , Y = y 1 y 1 y 2 … … y m X=x_1x_1x_2……x_n,Y=y_1y_1y_2……y_m X=x1x1x2……xn,Y=y1y1y2……ym,假设其最长公共子序列为 Z = z 1 z 2 … … z k Z = z_1z_2……z_k Z=z1z2……zk。
考虑 x n = y m x_n=y_m xn=ym时的情况,假设 x n = y m ≠ z k x_n=y_m\ne z_k xn=ym=zk,那么 LCS ( X , Y ) = LCS ( X n − 1 , Y m − 1 ) \text{LCS}(X,Y)=\text{LCS}(X_{n-1},Y_{m-1}) LCS(X,Y)=LCS(Xn−1,Ym−1),则存在 LCS ( X n − 1 , Y m − 1 ) + x n \text{LCS}(X_{n-1},Y_{m-1})+x_n LCS(Xn−1,Ym−1)+xn的更长的子序列,因此, x n = y m x_n=y_m xn=ym时, x n = y m = z k x_n=y_m=z_k xn=ym=zk。
当 x n ≠ y m x_n\ne y_m xn=ym时,可以得到 Z = max ( LCS ( X n − 1 , Y m ) , LCS ( X n , Y m − 1 ) ) Z=\max(\text{LCS}(X_{n-1},Y_m),\text{LCS}(X_{n},Y_{m-1})) Z=max(LCS(Xn−1,Ym),LCS(Xn,Ym−1))(取两者较长的那个)。
综上得到:
Z
=
{
LCS
(
X
n
−
1
,
Y
m
−
1
)
+
x
n
x
n
=
y
m
max
(
LCS
(
X
n
−
1
,
Y
m
)
,
LCS
(
X
n
,
Y
m
−
1
)
)
x
n
≠
y
m
Z=\begin{cases}\text{LCS}(X_{n-1},Y_{m-1})+x_n&x_n=y_m\\\max(\text{LCS}(X_{n-1},Y_m),\text{LCS}(X_{n},Y_{m-1}))& x_n\ne y_m\end{cases}
Z={LCS(Xn−1,Ym−1)+xnmax(LCS(Xn−1,Ym),LCS(Xn,Ym−1))xn=ymxn=ym
上述可以说明这个问题满足最优子结构特征。但是如果通过存储
LCS
(
X
i
,
Y
j
)
\text{LCS}(X_{i},Y_{j})
LCS(Xi,Yj)来自顶向上的求解该问题,存储开销较大。可以通过储存
LCS
(
X
i
,
Y
j
)
\text{LCS}(X_{i},Y_{j})
LCS(Xi,Yj)的长度,以及存储每一步是从哪来的标识,来解决这个问题,具体如下:
首先可以得到
c
[
i
,
j
]
=
{
0
i
∗
j
=
0
c
[
i
−
1
,
j
−
1
]
+
1
x
i
=
y
j
max
(
c
[
i
−
1
,
j
]
,
c
[
i
,
j
−
1
]
)
)
x
i
≠
y
j
c[i,j]=\begin{cases}0&i*j=0\\c[i-1,j-1]+1&x_i=y_j\\\max(c[i-1,j],c[i,j-1]))& x_i\ne y_j\end{cases}
c[i,j]=⎩⎪⎨⎪⎧0c[i−1,j−1]+1max(c[i−1,j],c[i,j−1]))i∗j=0xi=yjxi=yj
c
[
i
,
j
]
c[i,j]
c[i,j]表示
L
C
S
(
X
i
,
Y
j
)
LCS(X_i,Y_j)
LCS(Xi,Yj)的长度,其中
i
∗
j
=
0
i*j=0
i∗j=0时,表示空序列与任何序列的最长公共子序列长度为0。
另外为了构造出
L
C
S
(
X
,
Y
)
LCS(X,Y)
LCS(X,Y),做一个标识,有
s
[
i
,
j
]
=
{
−
1
i
∗
j
=
0
,
寻
找
尽
头
标
识
0
x
i
=
y
j
,
标
识
找
到
最
长
公
共
子
序
列
的
元
素
1
x
i
≠
y
j
,
c
[
i
−
1
,
j
]
>
c
[
i
,
j
−
1
]
,
标
识
下
一
个
元
素
应
在
L
C
S
(
X
i
−
1
,
Y
j
)
2
x
i
≠
y
j
,
c
[
i
−
1
,
j
]
<
c
[
i
,
j
−
1
]
,
标
识
下
一
个
元
素
应
在
L
C
S
(
X
i
,
Y
j
−
1
)
s[i,j]=\begin{cases}-1&i*j=0,寻找尽头标识\\0&x_i=y_j,标识找到最长公共子序列的元素\\1& x_i\ne y_j,c[i-1,j]>c[i,j-1],标识下一个元素应在LCS(X_{i-1},Y_j)\\2& x_i\ne y_j,c[i-1,j]<c[i,j-1],标识下一个元素应在LCS(X_{i},Y_{j-1})\\\end{cases}
s[i,j]=⎩⎪⎪⎪⎨⎪⎪⎪⎧−1012i∗j=0,寻找尽头标识xi=yj,标识找到最长公共子序列的元素xi=yj,c[i−1,j]>c[i,j−1],标识下一个元素应在LCS(Xi−1,Yj)xi=yj,c[i−1,j]<c[i,j−1],标识下一个元素应在LCS(Xi,Yj−1)
C++实现代码:
代码下载
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string LCS(string X,string Y){
if(X.size()==0||Y.size()==0) return string();
int n = X.size()+1,m = Y.size()+1;
int c[n][m],s[n][m];
//空序列与任何序列最长为0
for(int i=0;i<n;++i) {c[i][0] = 0; s[i][0] = -1;}
for(int i=0;i<m;++i) {c[0][i] = 0; s[0][i] = -1;}
//根据迭代公式,自底向上更新c,s数组
for (size_t i = 1; i < n; ++i)
{
for (size_t j = 1; j < m; ++j)
{
//判断此时序列尾字符是否相等
if(X[i-1]==Y[j-1]) {
c[i][j] = c[i-1][j-1]+1;
s[i][j] = 0;
}else{
if(c[i-1][j]>c[i][j-1]){
c[i][j] = c[i-1][j];
s[i][j] = 1;
}else{
c[i][j] = c[i][j-1];
s[i][j] = 2;
}
}
}
}
//通过c,s数组构造LCS
string ans(c[n-1][m-1],'a');
--n;--m;
while(s[n][m]!=-1){
if(s[n][m]==0) {ans[c[n][m]-1]=X[n-1];--n;--m;}
else if(s[n][m]==1){--n;}
else if(s[n][m]==2){--m;}
}
return ans;
}
int main(){
string X = "BCADB";
string Y = "BADB";
cout<<X<<"与"<<Y<<"的最长公共子序列:"<<endl;
cout<<LCS(X,Y)<<endl;
system("pause");
return 0;
}
Reference
《算法导论》,第十五章