11动态规划——最长公共子序列LCS问题
1.问题
DEF 1:
设X, Y是两个序列,其中 X = <x1, x2, … ,xn>, Z = <z1, z2, … ,zs>. 如果存在X的元素构成的按下标严格递增序列 <xi1, xi2, … ,xik>, 使 xij = zj, j = 1, 2, …, k ,那么称 Z 是 X 的子序列,Z 含有的元素个数 k,称为子序列的长度。
DEF 2:
设 X 和 Y 是两个序列,如果 Z 既是 X 的子序列,又是 Y 的子序列,则称 Z 是 X 和 Y 的公共子序列。
- 实例:序列 X=<A,B,D,C,A>, Y=<C,D,B,A,C>,
- 则 <A,C>,<B,C>,<B,A>等都是X和Y的公共子序列。
最长公共子序列问题:
给定序列 X = <x1, x2, … ,xn>,Y = <y1, y2, … ,ym>,基于上面两个定义,求最长公共子序列(Longest Common Subsequence, LCS)。
2.解析
Xi = <x1, x2, … ,xn>,i = 1, 2, …, n;
Yj = <y1, y2, … ,ym>,j = 1, 2, …, m;
Zk = <z1, z2, … ,zs>,k = 1, 2, …, s.
如果 Zk 是 Xi 和 Yj 的最长公共子序列,则满足如下优化法则:
- xi = yj,那么zk = xi = yj, Zk-1 是 Xi-1 和 Yj-1 的最长公共子序列。
- xi ≠ yj, zk ≠ xi,那么zk 是 Xi-1 和 Yj 的最长公共子序列。
- xi ≠ yj, zk ≠ yj,那么zk 是 Xi 和 Yj-1 的最长公共子序列。
如果 C[i,j] = a 表示 Xi 和 Yj 的最长公共子序列长度为 a, 则可以写出递推公式:
Z 的生成过程具化为 C[i,j] 的求值过程。
举个栗子
X = <A, B, B, D, A>,Y = <B, C, C, D, A, C>. n=[0, 5], m=[0, 6],求 X 和 Y 的最长公共子序列。
初始化(边界条件):
X = <A, B, B, D, A>,Y = <B, C, C, D, A, C>.
一、计算最长公共子序列长度算法
-
i = 1
a) j = 1,X.A <> Y.B:C[1, 1] = max(C[1, 0], C[0, 1]) = max(0, 0) = 0, 删除y;
b) j = 2,X.A <> Y.C:C[1, 2] = max(C[1, 1], C[0, 2]) = max(0, 0) = 0, 删除y;
c) j = 3,X.A <> Y.C:C[1, 3] = max(C[1, 2], C[0, 3]) = max(0, 0) = 0, 删除y;
d) j = 4,X.A <> Y.D:C[1, 4] = max(C[1, 3], C[0, 4]) = max(0, 0) = 0, 删除y;
e) j = 5,X.A == Y.A:C[1, 5] = C[0, 4] + 1 = 1, 删除 x 和 y;
f ) j = 6,X.A <> Y.C:C[1, 6] = max(C[1, 5], C[0, 6]) = max(1, 0) = 1, 删除y.
注:<> 表示不等于。 -
i = 2
a) j = 1,X.B == Y.B:C[2, 1] = C[1, 0] + 1 = 1, 删除 x 和 y;
b) j = 2,X.B <> Y.C:C[2, 2] = max(C[2, 1], C[1, 2]) = max(1, 0) = 1, 删除y;
c) j = 3,X.B <> Y.C:C[2, 3] = max(C[2, 2], C[1, 3]) = max(1, 0) = 1, 删除y;
d) j = 4,X.B <> Y.D:C[2, 4] = max(C[2, 3], C[1, 4]) = max(1, 0) = 1, 删除y;
e) j = 5,X.B <> Y.A:C[2, 5] = max(C[2, 4], C[1, 5]) = max(1, 1) = 1, 删除y;
f ) j = 6,X.B <> Y.C:C[2, 6] = max(C[2, 5], C[1, 6]) = max(1, 1) = 1, 删除y.
注:若max()中的两个值相等,都先选择删除y -
i = 3
a) j = 1,X.B == Y.B:C[3, 1] = C[2, 0] + 1 = 1, 删除 x 和 y;
b) j = 2,X.B <> Y.C:C[3, 2] = max(C[3, 1], C[2, 2]) = max(1, 1) = 1, 删除y;
c) j = 3,X.B <> Y.C:C[3, 3] = max(C[3, 2], C[2, 3]) = max(1, 1) = 1, 删除y;
d) j = 4,X.B <> Y.D:C[3, 4] = max(C[3, 3], C[2, 4]) = max(1, 1) = 1, 删除y;
e) j = 5,X.B <> Y.A:C[3, 5] = max(C[3, 4], C[2, 5]) = max(1, 1) = 1, 删除y;
f ) j = 6,X.B <> Y.C:C[3, 6] = max(C[3, 5], C[2, 6]) = max(1, 1) = 1, 删除y. -
i = 4
a) j = 1,X.D <> Y.B:C[4, 1] = max(C[4, 0], C[3, 1]) = max(0, 1) = 1, 删除x;
b) j = 2,X.D <> Y.C:C[4, 2] = max(C[4, 1], C[3, 2]) = max(1, 1) = 1, 删除y;
c) j = 3,X.D <> Y.C:C[4, 3] = max(C[4, 2], C[3, 3]) = max(1, 1) = 1, 删除y;
d) j = 4,X.D == Y.D:C[4, 4] = C[3, 3] + 1 = 2, 删除 x 和 y;
e) j = 5,X.D <> Y.A:C[4, 5] = max(C[4, 4], C[3, 5]) = max(2, 1) = 2, 删除y;
f ) j = 6,X.D <> Y.C:C[4, 6] = max(C[4, 5], C[3, 6]) = max(2, 1) = 2, 删除y. -
i = 5
a) j = 1,X.A <> Y.B:C[5, 1] = max(C[5, 0], C[4, 1]) = max(0, 1) = 1, 删除x;
b) j = 2,X.A <> Y.C:C[5, 2] = max(C[5, 1], C[4, 2]) = max(1, 1) = 2, 删除y;
c) j = 3,X.A <> Y.C:C[5, 3] = max(C[5, 2], C[4, 3]) = max(2, 1) = 2, 删除y;
d) j = 4,X.A <> Y.D:C[5, 4] = max(C[5, 3], C[4, 4]) = max(2, 2) = 2, 删除y;
e) j = 5,X.A == Y.A:C[5, 5] = C[4, 4] + 1 = 3, 删除 x 和 y;
f ) j = 6,X.A <> Y.C:C[5, 6] = max(C[5, 5], C[4, 6]) = max(3, 2) = 3, 删除y.
于是得到C[i, j]表:
此外,从C[5, 6] 开始,“删除 x” 对应表格向上移一格,“删除y”对应表格向左移一格,“删除x和y”对应表格向左移一格再向上移一格,我们得到记录删除对象的B[i, j]表:
二、输出最长子序列元素算法
X = <A, B, B, D, A>,Y = <B, C, C, D, A, C>.
查表得:
-
X = 5,Y = 6
B[5, 6] “2:删除y”
X = <A, B, B, D, A> => X = <A, B, B, D, A>
Y = <B, C, C, D, A, C> => Y = <B, C, C, D, A> -
X = 5,Y = 5
B[5, 5] “3:删除x、y”
X = <A, B, B, D, A> => X = <A, B, B, D>
Y = <B, C, C, D, A> => Y = <B, C, C, D>
入栈 A -
X = 4,Y = 4
B[4, 4] “3:删除x、y”
X = <A, B, B, D> => X = <A, B, B>
Y = <B, C, C, D> => Y = <B, C, C>
入栈 D -
X = 3,Y = 3
B[3, 3] “2:删除y”
X = <A, B, B> => X = <A, B, B>
Y = <B, C, C> => Y = <B, C> -
X = 3,Y = 2
B[3, 2] “2:删除y”
X = <A, B, B> => X = <A, B, B>
Y = <B, C> => Y = < B > -
X = 3,Y = 1
B[3, 1] “2:删除x、y”
X = <A, B, B> => X = <A, B>
Y = < B > => Y = < >
入栈 B
至此,序列Y已无元素,输出最长子序列元素算法结束。
再通过出栈依次打印Z元素,即在控制台上输出 X 和 Y 的最长公共子序列 Z <B, D, A>.
由于当 X.xi <> Y.yj ,且 C[i, j-1] == C[i-1, j] 时,该算法总是选择优先删除y,也就是将 C[i, j-1] 的值赋给 C[i, j],所以总会得到一个确定的输出序列。但事实上,选择删除y还是删除x都是允许的,这表明最长公共子序列不一定唯一(元素不同),该算法最多输出了其中的一种可能。但我们必须清楚,最长公共子序列的长度是唯一确定的。
3.设计
//计算最长公共子序列长度算法
C[0,j]=C[i,0]=0, 1<=i<=n,1<=j<=m
for i=1 to n do
for j=1 to m do
if xi=yj then
C[i,j] ← C[i-1,j-1]+1;
B[i,j] ← 3;//删除x、y
else then
if C[i,j-1]>=C[i-1,j] then
C[i,j] ← C[i,j-1];
B[i,j] ← 2;//删除y
else then
C[i,j] ← C[i-1,j];
B[i,j] ← 1;//删除x
end for j
end for i
//f(B,i,j)输出最长子序列元素算法
if i=0 or j=0 then
return;
if B[i,j]=3 then //删除两个
输出Xi;//或者入栈
f(B,i-1,j-1);
else if B[i,j]=2 then //删除y
f(B,i,j-1);
else then
f(B,i-1,j); //删除x
4.分析
证明动态规划的优化法则:
假设Z1序列任一个子序列 C1 本身一定不是相对于子序列的初始和结束状态的最优
决策序列。
那么根据假设,必然存在另外一个子序列 C2 优于一个子序列 C1,并且两个子序列相对于子序列的初始和结束状态一致。
将 Z1 中原子序列 C1 替换为 C2,获得 Z2,因为 C2 优于 C1,必然 Z2 优于 Z1,这与 Z1是最优决策序列矛盾(已知),因此假设错误,得证。
双重循环,LCS()=O(nm).
5.源码
#include<iostream>
#include<cstdlib>
#include<stack>
#define N 7//N=序列X的长度+2
#define M 8//M=序列Y的长度+2
using namespace std;
char sequence_X[N] = "ABBDA";//X序列
char sequence_Y[M] = "BCCDAC";//Y序列
//C[1][2]表示X长度为1、Y长度为2时两者最长公共子序列长度
int length_C[N][M] = { 0 };
//三个结果:1表示删除x,2表示y,3表示两者都删
//B[1][2]=1 表示X长度为1、Y长度为2时得到两者最长公共子序列长度,
//要从前一步从右往左删除一个x元素
int info_B[N][M] = { 0 };
//栈,保存最长公共子序列元素,以便正向输出
stack<char> sequence_R;
//最长公共子序列长度算法
void LCS() {
for (int i = 1; i < N-1; i++) {
for (int j = 1; j < M-1; j++) {
//字符相同
if (sequence_X[i-1] == sequence_Y[j-1]) {
//都删
length_C[i][j] = length_C[i - 1][j - 1] + 1;
info_B[i][j] = 3;
}
//字符不同
else {
if (length_C[i][j - 1] > length_C[i - 1][j]) {
//删Y
length_C[i][j] = length_C[i][j - 1];
info_B[i][j] = 2;
}
else {
//删X
length_C[i][j] = length_C[i - 1][j];
info_B[i][j] = 1;
}
}
}
}
}
//元素保存到栈
void collectInfo(int start,int end) {
if (start == 0 || end == 0) {
return;
}
//都删,X和Y各减一
if (info_B[start][end] == 3) {
//cout << sequence_X[start - 1]<<" ";
//按子序列从后往前输出,先将输出结果保存在栈sequence_R中
sequence_R.push(sequence_X[start - 1]);
collectInfo(start - 1, end-1);
}
//删X,X减一
else if (info_B[start][end] == 1) {
collectInfo(start - 1, end);
}
//删Y,Y减一
else {
collectInfo(start , end-1);
}
}
//从前往后输出最长公共子序列,即出栈
void printSequence() {
int num = sequence_R.size();
cout << "序列X为:" << sequence_X << endl;
cout << "序列Y为:" << sequence_Y << endl;
cout << "X与Y的最长公共子序列为:";
for (int i = 0; i < num; i++) {
cout << sequence_R.top();
sequence_R.pop();
}
cout << endl;
}
int main() {
LCS();
collectInfo(N-2,M-2);//N=序列X的长度+2,M=序列Y的长度+2
printSequence();
return 0;
}