动态规划_最长公共子序列
先介绍几个定义:
- 序列: 一个具有 严格次序 的对象元素的集合.
- 字符序列 。 字符对象的序列则为 字符序列 如 X X X={ x 1 , x 2 , x m x_1, x_2, x_m x1,x2,xm}.
- 子序列:给定序列 X X X={ x 1 , x 2 , x m x_1,x_2,x_m x1,x2,xm},序列 Z Z Z={ z 1 , z 2 , z k z_1, z_2, z_k z1,z2,zk}是 X X X的子序列,当且仅当存在一个严格递增的下标序列{ i 1 , i 2 , i k i_1,i_2,i_k i1,i2,ik}对 j ∈ j\in j∈{1,2,……,k},有 z j = x i j z_j= x_{i_j} zj=xij.
- 前缀 :给定 X X X={ x 1 , x 2 , x m x_1,x_2, x_m x1,x2,xm}子序列 X i X_i Xi={ x 1 , x 2 , … … , x i x_1,x_2,……,x_i x1,x2,……,xi}称为序列X的第i个前缀,其中 i ∈ i\in i∈{1,2,……,m}.
- 公共子序列 :给定两个序列X和Y当另一个序列 Z Z Z既是 X X X的子序列,又是 Y Y Y的子序列时,称 Z Z Z是 X X X和 Y Y Y的公共子序列.
- 最长公共子序列 (Longest Common Subsequence,LCS)问题:给定两个序列 X X X和 Y Y Y找出 X X X和 Y Y Y的一个最长公共子序列.
- 注意:最长公共子序列和最长连续公共子序列是两个概念,这里是最长公共子序列,即不要求是连续的,可以隔着取,所以在理解下面的算法时,不要弄混了这两个概念,造成误解
例 : 给定两个序列 X X X={B,C,B,D,A,B}和 Y Y Y={ D,C,A,B,A} 则:
- 序列{B,C,A}是 X X X和 Y Y Y的一个 公共子序列
- 序列{C,B,A}是 X X X和 Y Y Y的一个公共子序列,它的长度是4,而且它是 X X X和 Y Y Y的一个最长公共子序列.
- {C,A,B}也是最长公共子序列。可见,最长公共子序列并不唯一 。这里的 LCS 问题只需求出其中一个最长公共子序列即可.
算法思路:最优子结构与子问题重叠
设序列
X
X
X={
x
1
,
x
2
,
…
,
x
m
x_1, x_2,…,x_m
x1,x2,…,xm}.和
Y
Y
Y={
y
1
,
y
2
,
…
,
y
n
y_1,y_2,…,y_n
y1,y2,…,yn} 的最长公共子序列为
Z
Z
Z={
z
1
,
z
2
,
…
,
z
k
z_1,z_2,…,z_k
z1,z2,…,zk},则:
- 若 x m x_m xm= y n y_n 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} 和 和 和y_{n-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的最长公共子序列.
- 若 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的最长公共子序列.
- 以上,可以使用反证法证明
- 在计算 X X X和 Y Y Y的LCS时,可能需要计算 X m − 1 X_{m-1} Xm−1和 Y Y Y或 X X X和 Y n − 1 Y_{n-1} Yn−1的LCS很显然,都包含了 X m − 1 X_{m-1} Xm−1和 Y n − 1 Y_{n-1} Yn−1的LCS.
- 建立最优值的递归关系:
- 用arr[i][j].n记录序列 X i X_i Xi和 Y j Y_j Yj的LCS的长度。其中, X i X_i Xi={ x 1 , x 2 , … , x i x_1,x_2,…,x_i x1,x2,…,xi}和 Y j Y_j Yj={ y 1 , y 2 , … , y j y_1,y_2,…,y_j y1,y2,…,yj}分别为序列 X X X和 Y Y Y的第i个前缀和第j个前缀。
- 当i=0或j=0时,空序列是 X i X_i Xi和 Y j Y_j Yj的LCS。此时C[i][j]=0, 其他情况下,由最优子结构性质可建立递归关系。
- a r r [ i ] [ j ] . n = { 0 i=0,j=0 a r r [ i − 1 ] [ j − 1 ] . n + 1 i,j>0; x i = y j m a x { a r r [ i ] [ j − 1 ] . n , a r r [ i − 1 ] [ j ] . n } i,j>0; x i ≠ y j arr[i][j].n= \begin{cases} 0& \text{i=0,j=0}\\ arr[i-1][j-1].n + 1& \text{i,j>0;$x_i=y_j$}\\ max\{arr[i][j-1].n,arr[i-1][j].n\} & \text{i,j>0;$x_i\ne{y_j}$} \end{cases} arr[i][j].n=⎩⎪⎨⎪⎧0arr[i−1][j−1].n+1max{arr[i][j−1].n,arr[i−1][j].n}i=0,j=0i,j>0;xi=yji,j>0;xi=yj
求解步骤:
- Step1 确定合适的数据结构.采用二维数组arr.n来存放各个子问题的最优解。二维数组arr.mask记录各子问题最优值的来源。
- Step2 初始化。令
arr[i][0].n=0,arr[0][j].n=0
,其中 0 ≤ i ≤ m , 0 ≤ j ≤ n 0\leq i\leq m,0\leq j\leq n 0≤i≤m,0≤j≤n。 - Step3 循环阶段。根据递归关系式确定 X i X_i Xi和 Y j Y_j Yj的LCS的长度 0 ≤ i ≤ m 0\leq i\leq m 0≤i≤m。对于每个i循环 0 ≤ j ≤ n 0\leq j\leq n 0≤j≤n.
- Step4 根据arr.mask记录的信息以自底向上的方式来构造该LCS问题的最优解.
代码如下:
#include <iostream>
#include <cstring>
using namespace std;
#define RIGHT 0
#define DOWN 1
#define LOW_RIGHT 2
struct Node {
/**
* 当前位置i行j列
* a的前i个元素和b的前j个元素的最长公共子序列
*/
int n;
/**
* 标记是从哪走过来的,方便进行回溯
* RIGHT 从左边向右走过来的
* DOWN 从上向下走过来的
* LOW_RIGHT 从左上向右下走过来的
*/
int mask;
/*默认构造函数*/
Node() {
n = 0;
mask = -1;
}
};
//显示当前的标记数组n的信息
void disp_n(Node** arr, int i, int j) {
for (int tmp_i = 0; tmp_i < i; tmp_i++) {
for (int tmp_j = 0; tmp_j < j; tmp_j++) {
cout << arr[tmp_i][tmp_j].n << "\t";
}
cout << endl;
}
cout << "----------------------------------------------------" << endl;
}
//显示当前的标记数组mask的信息
void disp_dir(Node** arr, int i, int j) {
for (int tmp_i = 0; tmp_i < i; tmp_i++) {
for (int tmp_j = 0; tmp_j < j; tmp_j++) {
cout << arr[tmp_i][tmp_j].mask << "\t";
}
cout << endl;
}
cout << "---------------------------------------------------" << endl;
}
string LCS(const string &a, const string &b) {
/**
* 直接将a和b的长度进行计算,这样就不用每次调用的时候,还调用一遍,
* 这样能降低时间复杂度
*/
int len_a = a.length(), len_b = b.length();
// 记录和回溯数组
Node** arr = new Node * [len_a + 1];
for (int i = 0; i <= len_a; i++) {
arr[i] = new Node[len_b + 1];
}
/**
* 将第0行和第0列的n值都直接赋值为0,
* 因为,前(0,j)和(i,0)必定为0
* (0,j): a的前0个元素和b的前j个元素的最长公共子序列
* (i,0): b的前0个元素和a的前i个元素的最长公共子序列
*/
for (int i = 0; i <= len_a; i++) {
for (int j = 0; j <= len_b; j++) {
arr[i][j].n = 0;
}
}
for (int i = 1; i <= len_a; i++) {
for (int j = 1; j <= len_b; j++) {
/**
* i, j 都是从1开始的,所以为(i-1)和(j-1)
*/
if (a[i - 1] == b[j - 1]) {
/**
* arr[i - 1][j - 1].n : a的前(i-1)项和b的前(j-1)的项的最长公共子序列
* arr[i][j].n : a的前i项和b的前j项的最长公共子序列的和
*/
arr[i][j].n = arr[i - 1][j - 1].n + 1;
// 标记是从哪里走过来的
arr[i][j].mask = LOW_RIGHT;
}
else {
/**
* a[i] 和 b[j] 不相等时
* arr[i][j] = max{arr[i][j-1].n, arr[i-1][j].n}
*/
if (arr[i][j - 1].n > arr[i - 1][j].n ) {
arr[i][j].n = arr[i][j - 1].n;
// 标记从哪里走过来的
arr[i][j].mask = RIGHT;
}
else {
arr[i][j].n = arr[i - 1][j].n;
// 标记从哪里走过来的
arr[i][j].mask = DOWN;
}
}
}
}
// 显示arr的n信息
disp_n(arr, len_a + 1, len_b + 1);
// 显示arr的mask信息
disp_dir(arr, len_a + 1, len_b + 1);
// 最终返回的公共子序列的字符串
string lcs = "";
// 从最后一个位置开始回溯,在mask为-1时停止
for (int i = len_a, j = len_b; arr[i][j].mask != -1; ) {
// 从上向下走过来的
if (arr[i][j].mask == DOWN ) {
// 向上走回去
i--;
continue;
}
// 从左向右走过来的
if (arr[i][j].mask == RIGHT ) {
// 向左走回去
j--;
continue;
}
// 从左上向右下走过来的
if (arr[i][j].mask == LOW_RIGHT) {
// 这种情况发生就意味着a[i-1] = b[j-1]的成立,所以,属于最长公共子序列的元素,记录下来
lcs.push_back(a[i - 1]);
// 向左上走回去
i--;
j--;
continue;
}
}
// 释放内存空间
for (int i = 0; i <= len_a; i++) {
delete arr[i];
}
delete[]arr;
// 因为是回溯回去的,所以这里要将记录字符串翻转一下
reverse(lcs.begin(), lcs.end() );
return lcs;
}
int main() {
string a, b;
// 输入两字符串
cin >> a >> b;
// 调用函数,得到最长公共子串
string lcs = LCS(a, b);
cout << "最长公共子序列为 : " << lcs << endl;
cout << "长度为 : " << lcs.length() << endl;
return 0;
}
运行结果: