LCS最长公共子序列——动态规划(Dynamic Programming)
LCS最长公共子序列——动态规划(Dynamic Programming)
问题描述
给定
序列\(X=<x_1,x_2,x_3\dots x_m>\),
序列\(Y=<y_1,y_2,y_3\dots y_n>\)
两个序列,求它们的最长公共子序列(\(Longest\ Common\ Subsequence\)——LCS)
\(Z=<z_1,z_2,z_3\dots z_k>\)
测试样例
| Sample Input | Sample Output |
|---|---|
| ACTG | ACG |
| AAGCG | 3 |
问题分析
名词解释
子序列:子序列是指从父序列中选取一部分元素,不改变其中相对位置,但可以不连续的序列。
子串:子串是从一个父字符串中取出的连续的一段字符连成的字符串。
子串必定是子序列之一,但子序列不一定是子串。
如,有父串\(S=ABBACD\)
子序列\(s_1=ACD\)既是父串\(S\)的子串也是父串的子序列。
子序列\(s_2=ABCD\)只是父串\(S\)的子序列而不是子串(因为不连续)。
动态规划分析
自底向上——考虑最终解结构
设序列\(X\)长\(m\),序列\(Y\)长\(n\),
\(X_i=<x_1,x_2\dots x_i>\Tiny(0\leqslant i \leqslant m)\),
\(Y_j=<y_1,y_2\dots y_j>\Tiny(0\leqslant j \leqslant n)\),
\(X_i,Y_j\)的最长公共子序列为\(Z_{i,j}=<z_1,z_2 \dots z_k>\),
则最终解为\(Z_{m,n}\)。
在这个假设下状态就是\(i,j\)(子序列长度),阶段就是两个序列的长度。
我们需要寻找这个最终解是由什么决定的,因此我们可以从\(Z_{m-1,n}\),\(Z_{m,n-1}\),\(Z_{m-1,n-1}\)······这些上一状态的解里寻找转移方程。
- 从\(Z_{m-1,n-1}\)中得到最终解
假设对于\(X_m,Y_n\)和\(Z_{m,n}\),\(x_n=y_m\)可以通过反证法证明,必有\(z_k=x_n=y_m\),同时也可以得出\(Z_{m,n}=Z_{m-1,n-1}+z_k\),而\(Z_{m-1,n-1}\)可以通过求\(LCS(X_{m-1},Y_{n-1})\)得到。 - 从\(Z_{m-1,n}\)与\(Z_{m,n-1}\)中得到最终解
假设对于\(X_m,Y_n\)和\(Z_{m,n}\),\(x_n\neq y_m\)可以通过反证法证明,\(Z_{m,n}=max(Z_{m-1,n},Z_{m,n-1})\),而\(Z_{m-1,n},Z_{m,n-1}\)可以分别通过求\(LCS(X_{m-1},Y_{n})和LCS(X_{m},Y_{n-1})\)得到。
最终,我们可以发现对于任意两个序列m,n都可以递归的求解,我们将其带入动态规划的一般性方法。
阶段
这里的阶段指的是当前序列的长度\(L_{X_i}\)与\(L_{Y_j}\)。
状态
有两个状态,即X序列的前i个元素和Y序列的前j个元素。
状态转移方程
参考代码
#include "iostream"
#include "cstring"
#include "vector"
/*
ACTGTTGDF
ACGDDF
* */
using namespace std;
void getLCS(const string &X, const string &Y, string &Z, vector<vector<unsigned> > &dp) {
for (auto &it: dp[0]) it = 0;
for (auto &it: dp) it[0] = 0;//初始化
int Length = 0;
for (unsigned i = 1; i <= X.size(); i++) {//阶段
for (unsigned j = 1; j <= Y.size(); j++) {//阶段
if (X[i - 1] == Y[j - 1]) {//字符相同
dp[i][j] = dp[i - 1][j - 1] + 1;//状态转移方程
if (Length != dp[i][j]) {//根据Length取LCS
Z += X[i - 1];
++Length;
}
} else {
if (dp[i - 1][j] > dp[i][j - 1]) {//不等情况取舍
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 1];
}
}
}
}
}
int main() {
string A;//输入序列A
string B;//输入序列B
cin >> A >> B;
string Z;//LCS
vector<vector<unsigned> > dp(A.size() + 1);//dp数组
for (auto &it: dp) it.resize(B.size() + 1);
getLCS(A, B, Z, dp);
cout << Z << "\n" << dp[A.size()][B.size()];
return 0;
}
复杂度分析
时间复杂度分析
假设序列\(A\)长为\(n\),序列\(B\)长为\(m\),则算法要遍历计算完\(dp[m][n]\)整个数组。假设计算依次\(dp\)时间为\(T(1)\),则有时间复杂度计算公式:
不难看出该算法的时间复杂度约为\(O(n^2)\)。
空间复杂度
假设序列\(A\)长为\(n\),序列\(B\)长为\(m\),其耗费空间大小为\(m\times n\)的矩阵,其空间复杂度也为\(O(n^2)\)。

浙公网安备 33010602011771号