【多维动态规划】LC 1143.最长公共子序列

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

1143.最长公共子序列

2、题目描述

二、个人思路整理

1、思路分析

核心思路:二维动态规划

  1. 状态定义
    设 dp[i][j] 表示:text1 的前 i 个字符(即下标范围 0 到 i-1)与 text2 的前 j 个字符(即下标范围 0 到 j-1)的最长公共子序列长度。
    • 多开一位空间(大小设为 (m + 1) * (n + 1)),让下标 0 代表空字符串,可以避免繁琐的边界判断。
  2. 状态转移方程
    对于位置 (i, j),考察当前末尾字符:
    • 如果 text1[i - 1] == text2[j - 1]:说明这两个字符可以作为公共子序列的新末尾,状态直接由两边都不包含该字符的前缀转移而来: d p i j = d p i − 1 j − 1 + 1 dpij = dpi - 1j - 1 + 1 dpij=dpi−1j−1+1
    • 如果 text1[i - 1] != text2[j - 1]:说明这两个字符不能同时放在公共子序列末端。最长公共子序列要么来自 text1 前 i − 1 i-1 i−1 个字符与 text2 前 j j j 个字符,要么来自 text1 前 i i i 个字符与 text2 前 j − 1 j-1 j−1 个字符,取较大值即可: d p i j = max ⁡ ( d p i − 1 j , d p i j − 1 ) dpij = \max(dpi - 1j, dpij - 1) dpij=max(dpi−1j,dpij−1)
  3. 初始化与结果
    • 初始化:当其中一个字符串为空串(即 i = 0 i = 0 i=0 或 j = 0 j = 0 j=0)时,LCS 长度必然为 0 0 0,整个数组默认初始化为 0 0 0 即可。
    • 目标结果:dp[m][n],其中 m m m 和 n n n 分别是两个字符串的长度。
cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size();
        int n = text2.size();

        // dp[i][j] 表示 text1 的前 i 个字符(text[0..i-1])
        // 与 text2 的前 j 个字符(text[0..j-1])的最长公共子序列长度。
        // 维度开为 (m + 1) * (n + 1),方便处理空字符串(i=0 或 j=0)的边界情况。
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 当前字符相同:两个字符均可计入 LCS
                // 长度为除去这两个字符后的最长公共子序列长度加 1
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                // 当前字符不匹配:至少有一个字符不在当前的最长公共子序列中
                // 分别舍弃 text1[i-1] 或 text2[j-1],取两种情况的最大值
                else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        // 最终返回完整字符串 text1[0..m-1] 与 text2[0..n-1] 的LCS长度
        return dp[m][n];
    }
};

复杂度分析

  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n),双重循环遍历整个表格。
  • 空间复杂度:标准写法为 O ( m × n ) O(m \times n) O(m×n)。由于计算 dp[i][j] 仅依赖上一行 dp[i-1][...] 和当前行左侧的值,可以使用滚动数组优化到 O ( min ⁡ ( m , n ) ) O(\min(m, n)) O(min(m,n))。

三、知识风暴

动态规划(Dynamic Programming) 是本题的核心算法思想。它通过将原问题拆解为若干重叠子问题,并利用「最优子结构」性质,用子问题的最优解递推得到全局最优解。对于「最长公共子序列」这类序列型动态规划问题,动态规划能以 O ( m × n ) O(m \times n) O(m×n) 的复杂度高效求解。

算法核心思想:

  • 最优子结构 :dp[i][j] 表示 text1 的前 i i i 个字符与 text2 的前 j j j 个字符的最长公共子序列长度,可以由「当前末尾字符是否相等」这一条件,从 dp[i - 1][j - 1]、dp[i - 1][j]、dp[i][j - 1] 三个子状态递推得到。只要这些子问题已知,就能递推得到当前位置的最优解。
  • 重叠子问题 :在递推过程中,同一个状态 dp[i][j] 会被多个后续状态反复引用。例如计算 dp[i + 1][j] 与 dp[i][j + 1] 时,都会访问 dp[i][j] 的结果,因此用表格缓存结果可避免重复计算。
  • 与贪心的区别 :贪心每一步只做当前最优选择、不回溯;而动态规划会枚举「末尾字符是否相等」两种分支,分别考虑「舍弃 text1[i-1]」与「舍弃 text2[j-1]」两种情况并取最大值,从而保证结果的正确性。

常见对比:动态规划 vs 贪心

  • 动态规划 :时间复杂度 O ( m × n ) O(m \times n) O(m×n),空间复杂度 O ( m × n ) O(m \times n) O(m×n)(可优化到 O ( min ⁡ ( m , n ) ) O(\min(m, n)) O(min(m,n)))。适合需要同时考虑「匹配/不匹配」两种决策、且局部最优不能直接决定全局最优的场景,通用性更强。
  • 贪心算法 :时间复杂度 O ( m + n ) O(m + n) O(m+n),空间复杂度 O ( 1 ) O(1) O(1)。适合每一步的局部最优能直接推导全局最优的场景,代码简洁高效,但本题中「最长公共子序列」无法用贪心直接证明(例如贪心只追求当前字符匹配,就可能错过更长的公共子序列)。
  • 共同点:两者都依赖「最优子结构」性质。区别在于贪心只保留一个当前最优状态,而动态规划需要同时维护「匹配」与「不匹配」两种来源的状态。

动态规划的设计思想:

  • 核心思想 :把大问题拆成小问题,先解决小问题,再用小问题的答案拼出大问题的答案。本题中,先确定空串边界( i = 0 i = 0 i=0 或 j = 0 j = 0 j=0 时 d p = 0 dp = 0 dp=0),再逐个字符递推出后续位置的状态。
  • 与本题的联系 :最长公共子序列问题天然具有递推结构------每个状态 dp[i][j] 的最优解都可以由「末尾字符相等时 dp[i - 1][j - 1] + 1」与「末尾字符不等时 max(dp[i - 1][j], dp[i][j - 1])」共同得到。因此无需回溯或搜索,只需按顺序填表即可。
  • 注意事项 :动态规划的正确性依赖于「最优子结构」与「无后效性」。本题中 dp[i][j] 只由左上、上方与左方三个状态决定,与未来的状态无关,因此递推顺序合法。

使用要点:

  • 状态变量 :dp[i][j] 记录 text1 前 i i i 个字符与 text2 前 j j j 个字符的最长公共子序列长度( 0 ≤ i ≤ m 0 \le i \le m 0≤i≤m, 0 ≤ j ≤ n 0 \le j \le n 0≤j≤n),多开一位空间以统一处理空串边界。
  • 初始化 :dp[i][0] = 0、dp[0][j] = 0(任一字符串为空时 LCS 长度为 0 0 0),作为递推基准。
  • 转移时机 :外层循环遍历 text1 的每个字符 i i i,内层循环遍历 text2 的每个字符 j j j。当 text1[i - 1] == text2[j - 1] 时执行 dp[i][j] = dp[i - 1][j - 1] + 1;否则执行 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])。
  • 结果返回 :遍历结束后,返回 dp[m][n],表示两个完整字符串的最长公共子序列长度。

算法变体与扩展:

  • 最长公共子串(LeetCode 718):与 LCS 不同,子串要求连续。当字符不匹配时状态直接归零,而非取两侧最大值,递推结构略有差异。
  • 编辑距离(LeetCode 72):将「计数」改为「求最小编辑次数」,状态转移方程由取最大值改为取最小值,与本题的二维递推结构高度相似。
  • 不同的子序列(LeetCode 115):在本题基础上引入「计数」约束,需要同时考虑「匹配」与「跳过」两种情形。
  • 最长回文子序列(LeetCode 516):将两个字符串退化为同一个字符串的正反比较,通过二维区间 DP 刻画回文结构。

相关 LeetCode 例题:

相关推荐
C++ 老炮儿的技术栈1 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
Omics Pro1 小时前
全新可重分析!代谢组质谱专用
数据库·人工智能·算法·机器学习·自然语言处理
Nebula_g1 小时前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈
一只废狗狗狗狗狗狗狗狗狗1 小时前
Network架构1——卷积神经网络CNN
人工智能·算法·cnn
是希燃亚1 小时前
AI Agent 的未来,可能不是更强,而是更简单
算法·架构
alphaTao1 小时前
LeetCode 每日一题 2026/10/5-2026/10/11
算法·leetcode
liliangcsdn1 小时前
现金流指标的探索和分析
大数据·算法
wang09072 小时前
硬着头皮学数据结构和算法之Myers算法
数据结构·算法
蓝悦无人机2 小时前
无人机系列之飞控算法:从PID到AI,飞行控制的核心进化
人工智能·算法·无人机·飞控系统