【LeetCode Hot100 刷题日记 (94/100)】1143. 最长公共子序列 —— 字符串、动态规划(DP)(LCS)🧠

40 阅读4分钟

📌 题目链接:1143. 最长公共子序列 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:字符串、动态规划(DP)

⏱️ 目标时间复杂度:O(mn)

💾 空间复杂度:O(mn)(可优化至 O(min(m, n)))


在算法面试中,最长公共子序列(Longest Common Subsequence, LCS) 是一道经典中的经典。它不仅是动态规划的入门标杆题,更是理解“二维状态转移”、“子问题重叠”和“最优子结构”的绝佳载体。掌握本题,等于打通了 DP 思维的一条主干道!🚀

本文将带你 从零构建 DP 表,深入剖析状态定义与转移逻辑,并附上 C++ 与 JavaScript 双语言实现,助你彻底吃透这道高频面试题!


📌 题目分析

给定两个字符串 text1text2,要求找出它们的 最长公共子序列(LCS)的长度

✅ 注意:子序列 ≠ 子串

  • 子序列:字符相对顺序不变,但不必连续(如 "ace""abcde" 的子序列)
  • 子串:必须连续

例如:

  • text1 = "abcde", text2 = "ace" → LCS = "ace" → 长度为 3
  • text1 = "abc", text2 = "def" → 无公共子序列 → 返回 0

约束条件友好(长度 ≤ 1000),适合使用 O(mn) 的 DP 解法


🧩 核心算法及代码讲解:动态规划(LCS 模板)

LCS 是二维动态规划的标准模板题。其核心思想是:

dp[i][j] 表示 text1[0..i-1]text2[0..j-1] 的最长公共子序列长度

📐 状态定义

  • dp[i][j]:考虑 text1 的前 i 个字符 和 text2 的前 j 个字符 时,LCS 的长度

🔄 状态转移方程

分两种情况讨论:

  1. text1[i-1] == text2[j-1]
    → 当前字符匹配,可加入 LCS
    dp[i][j] = dp[i-1][j-1] + 1

  2. text1[i-1] != text2[j-1]
    → 当前字符不匹配,不能同时保留
    → 只能从两种“舍弃一个”的方案中选更优者:
    • 舍弃 text1[i-1]dp[i-1][j]
    • 舍弃 text2[j-1]dp[i][j-1]
    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

🧱 边界条件(Base Case)

  • dp[0][j] = 0(空串与任何串的 LCS 为 0)
  • dp[i][0] = 0

🎯 最终答案

  • dp[m][n],其中 m = text1.length(), n = text2.length()

🧭 解题思路(分步拆解)

  1. 初始化 DP 表
    创建 (m+1) × (n+1) 的二维数组,全部初始化为 0。

  2. 双重循环遍历两个字符串
    外层 i 从 1 到 m,内层 j 从 1 到 n。

  3. 判断当前字符是否相等

    • 相等 → 继承左上角值 +1
    • 不等 → 取上方或左方的最大值
  4. 返回右下角结果
    dp[m][n] 即为所求。

💡 关键洞察:DP 表的每个格子都代表一个子问题的最优解,最终汇聚成全局最优。


📊 算法分析

项目分析
时间复杂度O(mn) —— 每个 dp[i][j] 计算 O(1),共 mn 个状态
空间复杂度O(mn) —— 存储整个 DP 表✅ 可优化:由于只依赖前一行,可用滚动数组压缩至 O(n)
是否可回溯构造 LCS 字符串?✅ 可以!从 dp[m][n] 倒推路径即可(面试加分项)
常见变体- 最短公共超序列(SCS)- 编辑距离(Edit Distance)- 两个数组的 LCS(元素可重复)

🎯 面试高频考点

  • 能否手写 LCS DP 表?
  • 能否解释状态转移的逻辑?
  • 能否优化空间?
  • 能否输出具体的 LCS 字符串(而不仅是长度)?

💻 代码

✅ C++ 实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.length(), n = text2.length();
        // 创建 (m+1) x (n+1) 的 DP 表,初始化为 0
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        
        // 从 1 开始遍历,避免越界
        for (int i = 1; i <= m; i++) {
            char c1 = text1.at(i - 1);  // text1 的第 i 个字符(0-indexed)
            for (int j = 1; j <= n; j++) {
                char c2 = text2.at(j - 1);  // text2 的第 j 个字符
                
                if (c1 == c2) {
                    // 字符匹配:继承左上角 +1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 字符不匹配:取上方或左方的最大值
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];  // 返回最终结果
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    
    Solution sol;
    // 示例测试
    cout << sol.longestCommonSubsequence("abcde", "ace") << "\n";   // 输出: 3
    cout << sol.longestCommonSubsequence("abc", "abc") << "\n";     // 输出: 3
    cout << sol.longestCommonSubsequence("abc", "def") << "\n";     // 输出: 0
    
    return 0;
}

✅ JavaScript 实现

/**
 * @param {string} text1
 * @param {string} text2
 * @return {number}
 */
var longestCommonSubsequence = function(text1, text2) {
    const m = text1.length;
    const n = text2.length;
    
    // 初始化 (m+1) x (n+1) 的 DP 表
    const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        const c1 = text1[i - 1];
        for (let j = 1; j <= n; j++) {
            const c2 = text2[j - 1];
            
            if (c1 === c2) {
                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
console.log(longestCommonSubsequence("abc", "abc"));   // 3
console.log(longestCommonSubsequence("abc", "def"));   // 0

🔚 结语

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!