最长公共子序列

动态规划求解两个字符串的最长公共子序列

问题

给定两个字符串,找出它们的最长公共子序列(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] 开始,根据状态来源反向构建序列
  • 注意区分:子序列(不连续)和子串(连续)是不同问题