LeetCode 1143. 最长公共子序列
📌 题目描述
题目级别:中等
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
- 示例 1:
输入:text1 = "abcde",text2 = "ace"
输出:3
解释:最长公共子序列是"ace",它的长度为 3 。
💡 解法一:二维动态规划
面对两个字符串的子序列匹配问题,标准的起手式就是开辟一个二维 DP 数组。
状态定义:
定义 dp[i][j] 表示:text1 的前 i 个字符和 text2 的前 j 个字符,它们的最长公共子序列的长度。
状态转移方程:
当我们考察 text1[i] 和 text2[j] 时,只有两种情况:
- 两个字符相等 (
text1[i] == text2[j]) :
说明我们找到了一个公共字符!那么这个字符就可以接在它们前面的公共子序列之后。dp[i][j] = dp[i - 1][j - 1] + 1 - 两个字符不相等 (
text1[i] != text2[j]) :
说明这两个字符不可能同时出现在公共子序列的末尾。我们要么舍弃text1[i],看看text1[0...i-1]和text2[0...j]的匹配结果;要么舍弃text2[j],看看text1[0...i]和text2[0...j-1]的匹配结果。我们取这两种选择中的最大值。dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
本解法高光点(哨兵技巧):
代码中极其精妙地使用了 text1 = " " + text1;,在字符串头部人为加入了一个空格哨兵。
这样不仅让字符串的有效下标从 1 开始,更重要的是,当我们在代码中调用 i - 1 和 j - 1 时,绝对不会发生数组越界报错,彻底免去了繁琐的边界初始化判断!
💻 C++ 代码实现 (二维 DP)
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
// 极客技巧:加空格哨兵,让有效字符下标从 1 开始,完美避开 i-1 越界问题
text1 = " " + text1;
text2 = " " + text2;
// 开辟 DP 数组,多开 10 个空间防止溢出
int dp[m + 10][n + 10];
memset(dp, 0, sizeof dp); // 初始化为 0
// 遍历所有状态
for (int i = 1; i <= m; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
// 标准的状态转移
if (text1[i] == text2[j]) {
// 字符相等:取左上角的值 + 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];
}
};
进阶挑战:你能否将空间复杂度优化到 O(min(M,N))O(\min(M, N))O(min(M,N))?
💡 解法二:一维滚动数组降维打击
在经典的二维状态转移方程中:
dp[i][j] = dp[i - 1][j - 1] + 1dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
仔细观察会发现,我们在计算第 i 行的 dp 值时,仅仅依赖于第 i - 1 行(上一行)和第 i 行(当前行左边)的值。至于再往前的历史数据,我们根本不需要了。
因此,我们不需要维护整个巨大的二维矩阵,只需要一个长度为 N+1N+1N+1 的一维数组 dp 即可循环滚动!
唯一的难点在于 dp[i - 1][j - 1] :因为在更新一维数组时,当前位置的左上角的值(也就是还没被更新前的 dp[j-1])会被提前覆盖掉。所以我们需要一个临时变量 prev,把它在被覆盖前给保护起来,留给下一步使用。
💻 C++ 代码实现 (空间优化版)
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 让 text2 始终是较短的字符串,这样一维数组就可以开得更小
if (text1.size() < text2.size()) {
swap(text1, text2);
}
int m = text1.size(), n = text2.size();
// 只需要一个长度为 n + 1 的一维数组
vector<int> dp(n + 1, 0);
for (int i = 1; i <= m; i++) {
// prev 用来记录"左上角"的值 (dp[i-1][j-1])
int prev = dp[0];
for (int j = 1; j <= n; j++) {
// temp 先把正上方的值 (也就是将要被覆盖的旧 dp[j]) 保存下来,给下一轮的 prev 用
int temp = dp[j];
if (text1[i - 1] == text2[j - 1]) {
// 相等时,使用保护好的左上角旧值 + 1
dp[j] = prev + 1;
} else {
// 不等时,取上方 (旧 dp[j]) 和左方 (新 dp[j-1]) 的较大值
dp[j] = max(dp[j], dp[j - 1]);
}
// 将当前旧的 dp[j] 赋值给 prev,成为下一次循环的"左上角"
prev = temp;
}
}
return dp[n];
}
};