📌 题目链接:1143. 最长公共子序列 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:字符串、动态规划(DP)
⏱️ 目标时间复杂度:O(mn)
💾 空间复杂度:O(mn)(可优化至 O(min(m, n)))
在算法面试中,最长公共子序列(Longest Common Subsequence, LCS) 是一道经典中的经典。它不仅是动态规划的入门标杆题,更是理解“二维状态转移”、“子问题重叠”和“最优子结构”的绝佳载体。掌握本题,等于打通了 DP 思维的一条主干道!🚀
本文将带你 从零构建 DP 表,深入剖析状态定义与转移逻辑,并附上 C++ 与 JavaScript 双语言实现,助你彻底吃透这道高频面试题!
📌 题目分析
给定两个字符串 text1 和 text2,要求找出它们的 最长公共子序列(LCS)的长度。
✅ 注意:子序列 ≠ 子串!
- 子序列:字符相对顺序不变,但不必连续(如
"ace"是"abcde"的子序列)- 子串:必须连续
例如:
text1 = "abcde",text2 = "ace"→ LCS ="ace"→ 长度为 3text1 = "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 的长度
🔄 状态转移方程
分两种情况讨论:
-
若
text1[i-1] == text2[j-1]
→ 当前字符匹配,可加入 LCS
→dp[i][j] = dp[i-1][j-1] + 1 -
若
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()
🧭 解题思路(分步拆解)
-
初始化 DP 表
创建(m+1) × (n+1)的二维数组,全部初始化为 0。 -
双重循环遍历两个字符串
外层i从 1 到 m,内层j从 1 到 n。 -
判断当前字符是否相等
- 相等 → 继承左上角值 +1
- 不等 → 取上方或左方的最大值
-
返回右下角结果
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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!