最长公共子序列
题目:
一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列 X = { x 1 , x 2 , . . . , x m } X=\{x_1,x_2,...,x_m\} X={x1,x2,...,xm} ,则另一序列 Z = { z 1 , z 2 , . . . , z k } Z=\{z_1,z_2,...,z_k\} Z={z1,z2,...,zk} ,是X的子序列是指存在一个严格递增下标序列 { i 1 , i 2 , . . . i k } \{i_1,i_2,...i_k\} {i1,i2,...ik} 使得对于所有j=1,2,…,k,有: z j = x i j z_j=x_{i_j} zj=xij 。例如,序列 Z = { B , C , D , B } Z=\{B,C,D,B\} Z={B,C,D,B} 是序列 X = { A , B , C , B , D , A , B } X=\{A,B,C,B,D,A,B\} X={A,B,C,B,D,A,B} 的子序列,相应的递增下标序列为 { 2 , 3 , 5 , 7 } \{2,3,5,7\} {2,3,5,7} 。
给定两个序列X和Y,当另一序列Z既是X的子序列有是Y的子序列时,称Z是X和Y的公共子序列。现要找出满足条件的最长的公共子序列。
分析:
最简单粗暴的方法是穷举法,对X的所有子序列,检查它是否也是Y的子序列,从而确定它是否是X和Y的公共子序列。并在检查过程中记录最长的公共子序列。X的所有子序列都检查过后即可求出X和Y的最长公共子序列。X的每个子序列相应于下标集 {1,2,…,m} 的一个子集,因此有 2 m 2^m 2m个不同子序列,从而穷举搜索法需要指数时间。
最优子结构性质:
事实上,最长公共子序列问题具有最优子结构性质。
设序列 X = { x 1 , x 2 , . . . , x m } X=\{ x_1,x_2,...,x_m \} X={x1,x2,...,xm} 和 Y = { y 1 , y 2 , . . . , y n } Y=\{ y_1,y_2,...,y_n \} Y={y1,y2,...,yn} 的最长公共子序列为 Z = { z 1 , z 2 , . . . , z m } Z=\{ z_1,z_2,...,z_m \} Z={z1,z2,...,zm} ,则
- 若 x m = y n x_m=y_n xm=yn ,则 z k = x m = y n z_k=x_m=y_n zk=xm=yn ,且 Z k − 1 Z_{k-1} Zk−1 是 X m − 1 X_{m-1} Xm−1 和 Y n − 1 Y_{n-1} Yn−1 的最长公共子序列。
- 若 x m ≠ y n x_m \ne y_n xm=yn ,且 z k ≠ x m z_k \ne x_m zk=xm ,则 Z Z Z 是 X m − 1 X_{m-1} Xm−1 和 Y Y Y 的最长公共子序列。
- 若 x m ≠ y n x_m \ne y_n xm=yn ,且 z k ≠ y n z_k \ne y_n zk=yn ,则 Z Z Z 是 X X X 和 Y n − 1 Y_{n-1} Yn−1 的最长公共子序列。
证明:
- 反证法,若 z k ≠ x m z_k \neq x_m zk=xm ,则 { z 1 , z 2 , . . . , z k , x m } \{ z_1,z_2,...,z_k,x_m\} {z1,z2,...,zk,xm} 才是最长的公共子序列,与原先的结论矛盾,故有 z k = x m = y n z_k=x_m=y_n zk=xm=yn ,由此可得 Z k − 1 Z_{k-1} Zk−1 是 X m − 1 X_{m-1} Xm−1 和 Y n − 1 Y_{n-1} Yn−1 的最长公共子序列。
- 若 x m ≠ y n x_m \ne y_n xm=yn ,且 z k ≠ x m z_k \ne x_m zk=xm ,那么 z k z_k zk 可能存在在Y中,因为 z k ≠ x m z_k \ne x_m zk=xm 故 z k z_k zk 只能存在于 X m − 1 X_{m-1} Xm−1 中,故可证明。
- 类似2
由此可见,当满足 x m = y n x_m=y_n xm=yn 时可确定问题的解;当满足条件 x m ≠ y n x_m \neq y_n xm=yn 时,选取 X m − 1 X_{m-1} Xm−1 , Y Y Y 的最长公共子序列和 X X X , Y n − 1 Y_{n-1} Yn−1 的最长公共子序列中最长的子问题。因此,最长公共子序列问题具有最优子结构性质。
递归关系:
根据上述的最优子结构性质得出的递归关系如下:
c
[
i
]
[
j
]
=
{
0
i
>
0
;
j
=
0
c
[
i
−
1
]
[
j
−
1
]
+
1
i
,
j
>
0
;
x
i
=
y
i
max
{
c
[
i
]
[
j
−
1
]
,
c
[
i
−
1
]
[
j
]
}
i
,
j
>
0
;
x
i
≠
y
i
c[i][j]= \begin{cases} 0 && i>0;j=0 \\ c[i-1][j-1]+1 && i,j>0;x_i=y_i \\ \max\{ c[i][j-1],c[i-1][j] \} && i,j>0;x_i \neq y_i \end{cases}
c[i][j]=⎩⎪⎨⎪⎧0c[i−1][j−1]+1max{c[i][j−1],c[i−1][j]}i>0;j=0i,j>0;xi=yii,j>0;xi=yi
实现:
计算子序列长度:
-
记录长度和子问题的数组都从1开始,首先初始化数组:
// 初始化第一列,长度为0,自然公共子序列也为0 for(int i=0; i<= m; i++){ c[i][0] = 0; } // 初始化第一行,理由同上 for(int j=0; j<= n; j++){ c[0][j] = 0; }
-
根据最优子结构性质,可以一行一行填写
(m+1)*(n+1)
的表格,当对应位置的x[i] == y[j]
时,在原有基础上直接加1,将此子问题标记为1:if(x[i-1] == y[j-1]){ // 相等,子序列加1 c[i][j] = c[i-1][j-1]+1; b[i][j] = 1; }
-
当对应位置的
x[i] != y[j]
时,取max{c[i-1][j], c[i][j-1]}
,当c[i-1][j]
更大时将子问题标记为2,并令c[i][j] = c[i-1][j]
else if(c[i-1][j] >= c[i][j-1]){ // 不相等,取 c[i-1][j] c[i][j] = c[i-1][j]; b[i][j] = 2; }
当
c[i][j-1]
更大时将子问题标记为3,并令c[i][j] = c[i][j-1]
else{ // 不相等,取 c[i][j-1] c[i][j] = c[i][j-1]; b[i][j] = 3; }
-
最后取
c[m][n]
即为最长子序列长度
求最长公共子序列:
-
通过递归求得,边界条件为
m==0 || n==0
:if(m==0 || n==0){ return; }
-
当
b[m][n] == 1
时,说明该字符在公共子序列中,故可输出。为了正序输出,因此要先进入递归,此时的长度应该都减1:if(b[m][n] == 1){ lcs(m-1, n-1, x, b); printf("%c ", x[m-1]); // x是从0开始的,b从1开始 }
-
当
b[m][n] == 2
时,说明最优子序列在子问题(m-1, n)中,因此m-1后进入递归:else if(b[m][n] == 2){ lcs(m-1, n, x, b); }
-
当
b[m][n] == 3
时,说明最优子序列在子问题(m, n-1)中,因此n-1后进入递归:else{ lcs(m, n-1, x, b); }
完整代码:
#include<stdio.h>
/**
最长公共子序列长度
m: 序列X长度
n: 序列Y长度
x: 序列x
y: 序列y
c: 子序列长度最优解
b: 记录对应c的值是由哪个子问题的解得到
1:相等,取 c[i-1][j-1]
2:不相等,取 c[i-1][j]
3:不相等,取 c[i][j-1]
*/
void lcsLength(int m, int n, char *x, char *y, int **c, int**b){
// 初始化第一列,长度为0,自然公共子序列也为0
for(int i=0; i<= m; i++){
c[i][0] = 0;
}
// 初始化第一行,理由同上
for(int j=0; j<= n; j++){
c[0][j] = 0;
}
// 序列X
for(int i=1; i<=m; i++){
// 序列Y
for(int j=1; j<=n; j++){
if(x[i-1] == y[j-1]){
// 相等,子序列加1
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-1][j]
c[i][j] = c[i-1][j];
b[i][j] = 2;
} else{
// 不相等,取 c[i][j-1]
c[i][j] = c[i][j-1];
b[i][j] = 3;
}
}
}
}
/**
最长公共子序列
*/
void lcs(int m, int n, char *x, int **b){
if(m==0 || n==0){
return;
}
if(b[m][n] == 1){
lcs(m-1, n-1, x, b);
printf("%c ", x[m-1]); // x是从0开始的,b从1开始
} else if(b[m][n] == 2){
lcs(m-1, n, x, b);
} else{
lcs(m, n-1, x, b);
}
}
void main(){
char x[] = {'A', 'B', 'C', 'B', 'D', 'A', 'B'};
char y[] = {'B', 'D', 'C', 'A', 'B', 'A'};
int m = sizeof(x) / sizeof(char);
int n = sizeof(y) / sizeof(char);
int* c[m+1];
int* b[m+1];
for(int i=0; i<=m; i++){
c[i] = (int*) malloc(sizeof(int)*(n+1));
b[i] = (int*) malloc(sizeof(int)*(n+1));
}
lcsLength(m, n, x, y, c, b);
printf("c:\t\tb:\n");
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
printf("%d ", c[i][j]);
}
printf("\t");
for(int j=1; j<=n; j++){
printf("%d ", b[i][j]);
}
printf("\n");
}
printf("\n");
printf("最长子序列长度:%d\n", c[m][n]);
lcs(m, n, x, b);
}