最长公共子序列(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]) ]
- 不选 ( X[i-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
解释:
- 初始化 DP 数组:
dp[i][j]表示 ( X[0..i-1] ) 和 ( Y[0..j-1] ) 的 LCS 长度。 - 状态转移: 根据字符是否相等更新
dp[i][j]的值。 - 最终结果:
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 的长度,且可以通过空间优化进一步提高性能。