【Hot 100 刷题计划】 LeetCode 1143. 最长公共子序列 | C++ 二维DP 与 哨兵技巧

LeetCode 1143. 最长公共子序列

📌 题目描述

题目级别:中等

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 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] 时,只有两种情况:

  1. 两个字符相等 (text1[i] == text2[j])
    说明我们找到了一个公共字符!那么这个字符就可以接在它们前面的公共子序列之后。 dp[i][j] = dp[i - 1][j - 1] + 1
  2. 两个字符不相等 (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 - 1j - 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] + 1
  • dp[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];
    }
};
相关推荐
Allen_LVyingbo4 小时前
《狄拉克符号法50讲》习题与解析(下)
算法·决策树·机器学习·健康医疗·量子计算
豆沙糕4 小时前
大模型面试高频题:请详细讲解检索中的BM25算法
人工智能·算法
不才小强4 小时前
查找算法详解:二分查找
数据结构·算法
Hical_W4 小时前
告别回调地狱:在 C++ Web 框架中全面拥抱协程
c++·github
君义_noip4 小时前
信息学奥赛一本通 4164:【GESP2512七级】学习小组 | 洛谷 P14922 [GESP202512 七级] 学习小组
学习·算法·动态规划·gesp·信息学奥赛
MicroTech20254 小时前
微算法科技(NASDAQ :MLGO)面向区块链的系统的高效反量子晶格盲签名技术
科技·算法·区块链
炘爚4 小时前
C++多线程编程:join与detach的致命陷阱
c++
yuan199974 小时前
OpenCV ViBe 运动检测算法实现
人工智能·opencv·算法
小樱花的樱花4 小时前
4 文件选择对话框 QFileDialog
开发语言·c++·ui