Title

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}\)······这些上一状态的解里寻找转移方程。

  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})\)得到。
  2. \(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个元素。

状态转移方程

\[Z[i][j]= \begin {cases} 0&i=0\ or\ j=0,\\ Z[i-1][j-1]&X[i]=Y[j],\\ max(Z[i-1][j],Z[i][j-1])&X[i]\neq Y[j].\\ \end {cases} \]

参考代码

#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)\),则有时间复杂度计算公式:

\[T(mn)= \begin {cases} 0&m=0,n=0,\\ T(m)*T(n)&m>0,n>0. \end {cases} \]

不难看出该算法的时间复杂度约为\(O(n^2)\)

空间复杂度

假设序列\(A\)长为\(n\),序列\(B\)长为\(m\),其耗费空间大小为\(m\times n\)的矩阵,其空间复杂度也为\(O(n^2)\)

posted @ 2023-12-01 13:37  是胡某某啊  阅读(82)  评论(0)    收藏  举报