二刷 LeetCode:动态规划经典双题复盘

目录

[一、LeetCode 1143. 最长公共子序列](#一、LeetCode 1143. 最长公共子序列)

题目回顾

[核心思路(二维 DP)](#核心思路(二维 DP))

[Java 实现代码](#Java 实现代码)

二刷反思

[二、LeetCode 72. 编辑距离](#二、LeetCode 72. 编辑距离)

题目回顾

[核心思路(二维 DP)](#核心思路(二维 DP))

[Java 实现代码](#Java 实现代码)

二刷反思

[三、两道题的关联与 DP 思想总结](#三、两道题的关联与 DP 思想总结)


继续更新我的 LeetCode 二刷复盘,这次啃的是两道动态规划的经典标杆题:「最长公共子序列」和「编辑距离」。比起第一次硬套 DP 模板,这次终于把状态转移的底层逻辑给吃透了,趁热整理成博客,把关键思路和易错点沉淀下来。


一、LeetCode 1143. 最长公共子序列

题目回顾

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0。

  • 子序列:不要求连续,只要相对顺序不变即可

核心思路(二维 DP)

这道题是双序列 DP 的入门标杆,核心是用二维 DP 表记录两个字符串前i和前j个字符的最长公共子序列长度。

  • 状态定义:dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列长度
  • 状态转移方程:
    1. 如果 text1[i-1] == text2[j-1]:dp[i][j] = dp[i-1][j-1] + 1(当前字符匹配,子序列长度 + 1)
    2. 如果不相等:dp[i][j] = max(dp[i-1][j], dp[i][j-1])(取两个字符串分别去掉当前字符的最大值)
  • 初始化:dp[0][*] = 0,dp[*][0] = 0(空字符串的公共子序列长度为 0)

Java 实现代码

java

运行

复制代码
public int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length();
    int n = text2.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                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];
}

二刷反思

  • 第一次写的时候,只记住了转移方程,却没理解为什么要这么转移。这次才明白:不相等时取max,本质是在 **"删除 text1 当前字符" 和 "删除 text2 当前字符" 两个决策中,保留最优解 **。
  • 空间优化:可以用一维数组滚动更新,把空间复杂度从 O (mn) 降到 O (min (m,n)),核心是倒序遍历j,避免覆盖上一轮的状态。
  • 易错点:数组下标和字符串下标要区分开,dp[i][j]对应的是text1[i-1]和text2[j-1],很容易写错索引。

二、LeetCode 72. 编辑距离

题目回顾

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数。

  • 三种合法操作:插入一个字符、删除一个字符、替换一个字符

核心思路(二维 DP)

这道题是双序列 DP 的进阶,和「最长公共子序列」是一脉相承的,核心是用 DP 表记录将word1前i个字符转为word2前j个字符的最少操作数。

  • 状态定义:dp[i][j] 表示将 word1[0..i-1] 转换为 word2[0..j-1] 的最少操作次数
  • 状态转移方程:
    1. 如果 word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](字符匹配,无需操作)
    2. 如果不相等:dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,分别对应:
      • dp[i-1][j] + 1:删除word1的当前字符
      • dp[i][j-1] + 1:在word1中插入一个字符(等价于删除word2的当前字符)
      • dp[i-1][j-1] + 1:替换word1的当前字符为word2的当前字符
  • 初始化:
    • dp[0][j] = j:空字符串转为word2的前j个字符,需要j次插入
    • dp[i][0] = i:word1的前i个字符转为空字符串,需要i次删除

Java 实现代码

java

运行

复制代码
public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 初始化边界
    for (int i = 0; i <= m; i++) dp[i][0] = i;
    for (int j = 0; j <= n; j++) dp[0][j] = j;
    // 状态转移
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[m][n];
}

二刷反思

  • 第一次做的时候,很容易混淆三种操作对应的状态转移。这次才彻底理清:删除 / 插入 / 替换分别对应 DP 表中三个方向的状态,本质是从三种操作中选择代价最小的一种。
  • 空间优化:同样可以用一维数组优化,空间复杂度降到 O (min (m,n)),但需要额外变量保存dp[i-1][j-1]的值。
  • 易错点:初始化边界时,dp[i][0]和dp[0][j]不能写错,否则整个 DP 表的计算都会出错。

三、两道题的关联与 DP 思想总结

这两道题看似不同,底层的 DP 逻辑高度同源:

  1. 双序列 DP 的通用模板 :
    • 定义二维 DP 表,状态表示两个字符串前i/j个字符的子问题解
    • 状态转移只依赖左、上、左上三个方向的状态
    • 初始化处理空字符串的边界情况
  2. 核心区别 :
    • 最长公共子序列:求最大值,字符匹配时 + 1,不匹配时取 max
    • 编辑距离:求最小值,字符匹配时直接继承,不匹配时取 min+1
  3. 二刷的最大收获:终于从 "套模板" 升级到了 "懂逻辑",以后遇到类似的双序列 DP 题(比如「不同的子序列」「两个字符串的删除操作」),就能快速套用这套思路,而不是死记硬背。
相关推荐
wanderist.几秒前
CSP 第43次认证 T4:集合划分 —— 图染色与二分图判定
算法·图论
不做无法实现的梦~5 分钟前
基于 PX4 的全仓库架构、无人机算法解析与开发学习规划
算法·架构·无人机
做cv的小昊10 分钟前
【大模型算法自学笔记01】NLP基础知识(1.1 自注意力)
人工智能·笔记·算法·自然语言处理·大模型·llm
stolentime35 分钟前
AT_arc182_a [ARC182A] Chmax Rush!题解
数据结构·c++·算法·动态规划
longlongzihan36 分钟前
LeetCode 189. 轮转数组:从直观模拟到最优原地算法
c++·算法·leetcode
爱喝热水的呀哈喽41 分钟前
cpp二分库函数 lambda捕获
算法
longlongzihan1 小时前
LeetCode 76. 最小覆盖子串:滑动窗口算法详解
算法·leetcode·滑动窗口
怕浪猫10 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试
无线通信科研笔记10 小时前
IEEE TVT 2026 论文精读与完整复现|相位误差如何重塑近场 RIS 的幅相响应
论文阅读·人工智能·python·算法·论文笔记
mmmmath_311 小时前
二叉树の递归遍历
算法