最长公共子序列(Longest Common Subsequence, LCS)

410 阅读3分钟

最长公共子序列(Longest Common Subsequence, LCS) 是指在两个序列中,找出一个既是两个序列的子序列,又是它们的最长子序列。不同于子串(要求连续),子序列可以是不连续的。

问题描述:

给定两个序列 ( X = x_1, x_2, ..., x_m ) 和 ( Y = y_1, y_2, ..., y_n ),我们要找出它们的最长公共子序列 ( LCS(X, Y) ),并返回其长度。子序列的顺序必须保持一致。

例子:

示例 1:

输入:

  • ( X = \text{"ABCBDAB"} )
  • ( Y = \text{"BDCAB"} )

输出:

  • LCS 的长度 = 4
  • LCS = "BDAB"(或 "BCAB")

示例 2:

输入:

  • ( X = \text{"AXYT"} )
  • ( Y = \text{"AYZX"} )

输出:

  • LCS 的长度 = 2
  • LCS = "AY"

动态规划解法

这个问题可以通过动态规划(DP)来解决,构建一个二维的 DP 表来存储中间状态。具体步骤如下:

1. 状态定义:

我们用二维数组 dp[i][j] 来表示 ( X[1..i] ) 和 ( Y[1..j] ) 的最长公共子序列的长度。这里的 ( i ) 和 ( j ) 分别表示在序列 ( X ) 和 ( Y ) 中的下标。

2. 状态转移:

  • 如果 ( X[i-1] == Y[j-1] ),说明当前字符相同,可以将其加入公共子序列,因此: [ dp[i][j] = dp[i-1][j-1] + 1 ]
  • 如果 ( X[i-1] \neq Y[j-1] ),则当前字符不同,不能同时出现在公共子序列中,我们有两种选择:
    • 不选 ( X[i-1] ),即取 dp[i-1][j]
    • 不选 ( Y[j-1] ),即取 dp[i][j-1] 取这两者中的最大值: [ dp[i][j] = \max(dp[i-1][j], dp[i][j-1]) ]

3. 边界条件:

  • 当 ( i = 0 ) 或 ( j = 0 ) 时,表示一个序列为空,此时 ( dp[i][0] = 0 ) 和 ( dp[0][j] = 0 ),因为任何空序列与另一个序列的公共子序列长度为 0。

4. 最终结果:

最终的 LCS 长度就是 ( dp[m][n] ),其中 ( m ) 和 ( n ) 是序列 ( X ) 和 ( Y ) 的长度。

动态规划实现

function longestCommonSubsequence(X, Y) {
    const m = X.length;
    const n = Y.length;
    
    // 创建二维 DP 数组,初始化为 0
    const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
    
    // 填充 DP 表
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (X[i - 1] === Y[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;  // 当前字符相同
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);  // 当前字符不同
            }
        }
    }
    
    // 返回 LCS 的长度
    return dp[m][n];
}

// 示例
const X = "ABCBDAB";
const Y = "BDCAB";
console.log(longestCommonSubsequence(X, Y));  // 输出 4

解释:

  1. 初始化 DP 数组: dp[i][j] 表示 ( X[0..i-1] ) 和 ( Y[0..j-1] ) 的 LCS 长度。
  2. 状态转移: 根据字符是否相等更新 dp[i][j] 的值。
  3. 最终结果: dp[m][n] 存储了两个序列的最长公共子序列的长度。

复杂度分析:

  • 时间复杂度: 动态规划的时间复杂度是 ( O(m \times n) ),其中 ( m ) 和 ( n ) 分别是两个字符串的长度,因为我们需要遍历整个二维 DP 表。

  • 空间复杂度: 动态规划的空间复杂度是 ( O(m \times n) ),因为我们使用了一个大小为 ( (m+1) \times (n+1) ) 的二维数组来存储中间结果。

空间优化:

可以使用滚动数组优化空间复杂度。由于每次计算 ( dp[i][j] ) 只与当前行和上一行有关,我们可以只保留当前行和上一行的值,从而将空间复杂度降低到 ( O(\min(m, n)) )。

代码实现(空间优化版):

function longestCommonSubsequence(X, Y) {
    const m = X.length;
    const n = Y.length;
    
    // 初始化两个数组来存储当前行和上一行的结果
    const prev = Array(n + 1).fill(0);
    const curr = Array(n + 1).fill(0);
    
    // 填充 DP 数组
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (X[i - 1] === Y[j - 1]) {
                curr[j] = prev[j - 1] + 1;
            } else {
                curr[j] = Math.max(prev[j], curr[j - 1]);
            }
        }
        // 交换 prev 和 curr
        [prev, curr] = [curr, prev];
    }
    
    // 返回 LCS 的长度
    return prev[n];
}

// 示例
const X = "ABCBDAB";
const Y = "BDCAB";
console.log(longestCommonSubsequence(X, Y));  // 输出 4

结论:

  • 最长公共子序列问题是经典的动态规划问题,适用于解决两个序列之间的匹配、比较问题。
  • 通过 DP 表的构建和状态转移,可以高效地计算出 LCS 的长度,且可以通过空间优化进一步提高性能。