最长公共子序列

问题描述

子序列:将当前序列去掉零个或多个元素所得序列。

公共子序列:同时满足是两个序列的子序列的序列。

最长公共子序列问题:求解两个序列的公共子序列,使得它尽可能长。

形式化定义

输入:

  • 序列 $X = \langle x_1, x_2, \cdots, x_n \rangle$
  • 序列 $Y = \langle y_1, y_2, \cdots, y_m \rangle$

输出:

  • 公共子序列 $Z = \langle z_1, z_2, \cdots, z_l \rangle$

优化目标:

$$ \begin{align} \max \left | Z \right | \end{align} $$

约束条件:

$$ \begin{align} \mathbf{s.t.} \langle z_{1}, z_{2}, \cdots, z_{l} \rangle = \langle x_{i_{1}}, x_{i_{2}}, \cdots, x_{i_{l}} \rangle = \langle y_{j_{1}}, y_{j_{2}}, \cdots, y_{j_{l}} \rangle \\ \left({1} \leq {i}_{1}<{i}_{2}, \cdots, {i}_{l} \leq {n} ; {1} \leq {j}_{1}<{j}_{2}, \cdots, {j}_{l} \leq {m}\right) \end{align} $$

问题分析

令 $C[i,j]$ 表示 $X[1:i]$ 和 $Y[1:j]$ 的最长公共子序列长度,那么原始问题就是求解 $C[n,m]$。

下面我们来考察末尾字符,分为如下两大类情况:

情况一

如果 $x_i \ne y_j$,那么需要考察 $C[i,j-1]$ 和 $C[i-1,j]$ 两个子问题。并且当前问题的解是这两个子问题解的最大值,即如下的状态转移方程:

$$ \begin{align} C[i,j] = \max \left \{ C[i-1,j], C[i,j-1] \right \} \end{align} $$

情况二

如果 $x_i = y_j$,那么既可以匹配这最后一对字符,也可以不匹配。这样一来,需要考察 $C[i-1,j-1]$、$C[i-1,j]$ 和 $C[i,j-1]$ 三个子问题。

问题:三个子问题是否都需要求解?

分析

  • $C[i-1,j]$ 比 $C[i-1,j-1]$ 至多大 $1$,即 $C[i-1,j] - C[i-1,j-1] \leqslant 1$。
  • $C[i,j-1]$ 比 $C[i-1,j-1]$ 至多大 $1$,即 $C[i,j-1] - C[i-1,j-1] \leqslant 1$。

因此

$$ \begin{align} C[i-1,j-1] + 1 \geqslant \max \left \{ C[i-1,j], C[i,j-1] \right \} \end{align} $$

故只需要考虑子问题 $C[i-1,j-1]$ 即可。于是状态转移方程为

$$ \begin{align} C[i,j] = C[i-1,j-1] + 1 \end{align} $$

小结

综上所述,状态转移方程为

$$ \begin{align} C[i,j] = \left \{ \begin{matrix} \max \left \{ C[i-1,j], C[i,j-1] \right \} & (x_i=y_j) \\\\ C[i-1,j-1] + 1 & (x_i \ne y_j) \end{matrix}\right . \end{align} $$

算法代码

string longestCommonSubsequence(const string &x, const string &y) {
    enum Drct {
        LU, // 向左上方回溯
        U,  // 向上方回溯
        L   // 向左边回溯
    };
    auto lengthX = x.size(), lengthY = y.size();
    // C[i,j] 表示 x[1..i] 和 y[1..j] 的最长公共子序列长度
    vector<vector<int>> C(lengthX + 1, vector<int>(lengthY + 1, 0));
    // rec[i,j] 是追踪数组,记录子问题来源
    vector<vector<Drct>> rec(lengthX + 1, vector<Drct>(lengthY + 1));

    for (int i = 1; i <= lengthX; ++i) {
        for (int j = 1; j <= lengthY; ++j) {
            if (x[i - 1] == y[j - 1]) {
                C[i][j] = C[i - 1][j - 1] + 1;
                rec[i][j] = LU;
            } else {
                C[i][j] = C[i - 1][j] > C[i][j - 1] ? C[i - 1][j] : C[i][j - 1];
                rec[i][j] = C[i - 1][j] > C[i][j - 1] ? U : L;
            }
        }
    }

    int maxLength = (lengthX < lengthY ? lengthX : lengthY) + 1;
    char *pStr = new char[maxLength];
    char *p = pStr + maxLength - 1;
    *p = '\0'; // 字符串末尾结束符

    // 根据追踪数组回溯最长公共子序列
    for (int i = lengthX, j = lengthY; i > 0 && j > 0;) {
        if (rec[i][j] == LU) {
            *(--p) = x[i - 1]; // LU 才说明 x[i]==y[j],该字符是最长公共子序列的一部分
            --i;
            --j;
        } else if (rec[i][j] == L) {
            --j;
        } else if (rec[i][j] == U) {
            --i;
        }
    }

    return string(p);
}

算法分析

  • 时间复杂度:$O(n^2)$
  • 空间复杂度:$O(n^2)$
Previous
Next