最长公共子序列
双序列 DP 经典题:`dp[i][j]` 表示两个前缀的最长公共子序列长度。
#resource / algorithm
#type / snippet
#status / growing
#source / leetcode
#algo / dp
[!info] related notes 算法面试题型 MOC 动态规划 网格 DP 与双序列 DP 编辑距离 不同的子序列 最长递增子序列(对比:以 i 结尾 vs 前 i 个)
最长公共子序列
题目链接:https://leetcode.cn/problems/longest-common-subsequence/
题意
给你两个字符串 text1 和 text2,求它们的最长公共子序列长度。
为什么是双序列 DP
每次都在比较两个前缀:
text1的前i个字符text2的前j个字符
状态
dp[i][j] 表示 text1 的前 i 个字符与 text2 的前 j 个字符的最长公共子序列长度。
转移
如果最后一个字符相同:
dp[i][j] = dp[i - 1][j - 1] + 1
否则:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
边界
任意一个字符串为空时,答案都是 0。
样例
text1 = "abcde", text2 = "ace"
最长公共子序列是 ace,长度为 3。
JavaScript
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[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]);
}
}
}
return dp[m][n];
};
易错点
i、j是前缀长度,不是字符串下标;访问字符时要用i - 1、j - 1- 这题是“前 i 个元素”模型,不是“以 i 结尾”模型
- 不要和子串(substring)搞混:子序列可以不连续,子串必须连续
空间优化
dp[i][j] 只依赖 dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1],可以压成一维数组。
注意:内层 j 必须倒序遍历,否则 dp[j-1] 已经被当前行覆盖,不再是上一行的值。
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = new Array(n + 1).fill(0);
for (let i = 1; i <= m; i++) {
let prev = 0;
for (let j = 1; j <= n; j++) {
const temp = dp[j];
if (text1[i - 1] === text2[j - 1]) {
dp[j] = prev + 1;
} else {
dp[j] = Math.max(dp[j], dp[j - 1]);
}
prev = temp;
}
}
return dp[n];
};
与 LIS 的对比
| 题目 | 状态定义 | 维度 | 模型 |
|---|---|---|---|
| LIS | dp[i]:以 nums[i] 结尾的最长递增子序列 | 一维 | 以 i 结尾 |
| LCS | dp[i][j]:两个前缀的最长公共子序列 | 二维 | 前 i 个元素 |
LIS 枚举 j < i 转移;LCS 根据当前字符是否相等转移。