最长公共子序列
动态规划求解两个字符串的最长公共子序列
问题
给定两个字符串,找出它们的最长公共子序列(Longest Common Subsequence,LCS)的长度。
子序列不要求连续,但要保持相对顺序。例如 "ace" 是 "abcde" 的子序列。
解答
动态规划解法
/**
* 求两个字符串的最长公共子序列长度
* @param {string} text1
* @param {string} text2
* @return {number}
*/
function longestCommonSubsequence(text1, text2) {
const m = text1.length;
const n = text2.length;
// dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的 LCS 长度
const dp = Array.from({ length: m + 1 }, () => 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]) {
// 字符相等,LCS 长度 +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];
}
// 测试
console.log(longestCommonSubsequence('abcde', 'ace')); // 3 ("ace")
console.log(longestCommonSubsequence('abc', 'def')); // 0
console.log(longestCommonSubsequence('abc', 'abc')); // 3
获取具体的子序列
/**
* 获取最长公共子序列的具体内容
* @param {string} text1
* @param {string} text2
* @return {string}
*/
function getLCS(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
// 构建 dp 表
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]);
}
}
}
// 回溯找出具体序列
let i = m,
j = n;
let result = '';
while (i > 0 && j > 0) {
if (text1[i - 1] === text2[j - 1]) {
// 当前字符是 LCS 的一部分
result = text1[i - 1] + result;
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return result;
}
// 测试
console.log(getLCS('abcde', 'ace')); // "ace"
console.log(getLCS('ABCDGH', 'AEDFHR')); // "ADH"
空间优化版本
/**
* 空间优化:只用两行数组
* @param {string} text1
* @param {string} text2
* @return {number}
*/
function lcsOptimized(text1, text2) {
const m = text1.length;
const n = text2.length;
// 只保留两行
let prev = Array(n + 1).fill(0);
let curr = 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]) {
curr[j] = prev[j - 1] + 1;
} else {
curr[j] = Math.max(prev[j], curr[j - 1]);
}
}
// 交换两行
[prev, curr] = [curr, prev];
}
return prev[n];
}
关键点
- 状态定义:
dp[i][j]表示text1[0..i-1]和text2[0..j-1]的 LCS 长度 - 状态转移:字符相等时
dp[i-1][j-1] + 1,不等时取max(dp[i-1][j], dp[i][j-1]) - 时间复杂度:O(m × n),空间复杂度 O(m × n),优化后 O(n)
- 回溯路径:从
dp[m][n]开始,根据状态来源反向构建序列 - 注意区分:子序列(不连续)和子串(连续)是不同问题
目录