【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];
    }
};
相关推荐
生物信息与育种1 天前
黄三文院士领衔植物星球计划(PLANeT)发表Cell
人工智能·深度学习·算法·面试·transformer
aini_lovee1 天前
WSN 四大经典无需测距定位算法
算法
人道领域1 天前
【LeetCode刷题日记】掌握二叉树遍历:栈实现的三种绝妙方法
算法·leetcode·职场和发展
北冥湖畔的燕雀1 天前
深入解析Linux信号处理机制
算法
阿Y加油吧1 天前
二刷 LeetCode:动态规划经典双题复盘
算法·leetcode·动态规划
上弦月-编程1 天前
C语言指针超详细教程——从入门到精通(面向初学者)
java·数据结构·算法
莫等闲-1 天前
代码随想录一刷记录Day44——leetcode1143.最长公共子序列 53. 最大子序和
数据结构·c++·算法·leetcode·动态规划
生成论实验室1 天前
《事件关系阴阳博弈动力学:识势应势之道》第七篇:社会与情感关系——连接、表达与共鸣
人工智能·算法·架构·交互·创业创新
承渊政道1 天前
【动态规划算法】(背包问题经典模型与解题套路)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
weixin_421725261 天前
2026年C/C++/C#全解析:底层语言的进化与场景抉择,选错直接掉队
c语言·c++·c·编程语言·技术选择