【中等】1143. 最长公共子序列

0 阅读3分钟

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 **是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入: text1 = "abcde", text2 = "ace" 
输出: 3  
解释: 最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入: text1 = "abc", text2 = "abc"
输出: 3
解释: 最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入: text1 = "abc", text2 = "def"
输出: 0
解释: 两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成。

🏠 生活案例:寻找共同的“回忆”

想象你和你的好朋友都有一个存钱罐。

  • 你每天往里存一块写着字母的积木,顺序是:A -> B -> C -> D -> E
  • 你朋友每天也存,顺序是:A -> C -> E

现在你们想玩个游戏:不改变积木排队的先后顺序,看看你们俩的存钱罐里,最多能凑出多少个一模一样且顺序一致的积木?

在这个例子里,你们共同拥有的积木是 ACE,长度是 3。这就是“最长公共子序列”。

注意:子序列不要求连续。就像你朋友虽然跳过了 B,但 A 依然在 C 前面,这就算匹配成功!


💻 代码实现与生活化注释

这又是一个动态规划的经典案例。我们准备一张二维表格,横轴是你朋友的积木,纵轴是你的积木。

JavaScript

/**
 * @param {string} text1
 * @param {string} text2
 * @return {number}
 */
var longestCommonSubsequence = function(text1, text2) {
    let numOne = text1.length;
    let numTwo = text2.length;

    // 1. 准备一张“匹配记录表” dp[numOne + 1][numTwo + 1]
    // dp[i][j] 的意思是:你的前 i 个积木和你朋友的前 j 个积木,能匹配出几个?
    let dp = new Array(numOne + 1).fill(0).map(() => Array(numTwo + 1).fill(0));

    // 2. 开始挨个比对
    for (let i = 1; i <= numOne; i++) {
        for (let j = 1; j <= numTwo; j++) {
            // 如果现在的这两个积木刚好一样!
            if (text1[i - 1] === text2[j - 1]) {
                // 就像找到了一个共同回忆:
                // 现在的匹配数 = “还没拿这两个积木前”的匹配数 + 1
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                // 如果不一样,那就看看之前的记录:
                // 要么是你退一步看之前的,要么是你朋友退一步看之前的
                // 我们选一个匹配数更多的结果保留下来
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    // 3. 填完表后,右下角的数字就是你们最终最长的共同回忆长度
    return dp[numOne][numTwo];
};

🧩 为什么代码里要 +1 还要取 max

我们可以把填表的过程想象成在迷宫里寻宝:

  1. text1[i-1] == text2[j-1]

    你发现了一个宝贝(相同的字母)!你必须从左上方(dp[i-1][j-1])斜着走过来,并把宝物捡起来(+1)。这代表这个字母被你们共同锁定了。

  2. text1[i-1] != text2[j-1]

    当前这两个字母没缘分。那你就得看看:是“我不拿这块积木时”的收获多,还是“你没拿这块积木时”的收获多?(取 Math.max)。


复杂度分析

  • 时间复杂度O(M×N)O(M \times N)MMNN 是两个字符串的长度。你需要遍历表格里的每一个格子。
  • 空间复杂度O(M×N)O(M \times N)。需要这张二维表来存储过程。

小知识:这个算法在现实中非常有用!比如著名的 git diff 命令(查看代码哪里改动了)或者论文查重系统,底层逻辑都有 LCS 的影子。