两道字符串 DP 模板题复盘:最长公共子序列 & 编辑距离

前言

今天复盘两道字符串动态规划的 "天花板级模板题"------「最长公共子序列」「编辑距离」。它们是双字符串 DP 的经典代表,掌握了这两道题的解法,大部分字符串 DP 问题都能找到思路。


一、1143. 最长公共子序列(中等)

题目描述

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

核心思路:二维动态规划

这道题的核心是定义状态并利用 "选或不选" 的思想转移:

  • 定义 dp[i][j] 表示 text1[0..i-1]text2[0..j-1] 的最长公共子序列长度。
  • 状态转移:
    • 如果 text1[i-1] == text2[j-1]dp[i][j] = dp[i-1][j-1] + 1(当前字符匹配,长度 + 1)
    • 如果不相等:dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])(选其中一个字符串的前一个状态)
  • 边界条件:dp[0][j] = 0dp[i][0] = 0(空字符串的公共子序列长度为 0)

完整代码(Java)

java

运行

复制代码
class Solution {
    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];
    }
}

优化:一维 DP

因为每次更新 dp[i][j] 只需要上一行的 dp[i-1][j] 和当前行的 dp[i][j-1],可以将二维数组压缩为一维,空间复杂度从 O (mn) 降到 O (min (m,n)):

java

运行

复制代码
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        if (text1.length() < text2.length()) {
            return longestCommonSubsequence(text2, text1);
        }
        int m = text1.length();
        int n = text2.length();
        int[] dp = new int[n + 1];
        for (int i = 1; i <= m; i++) {
            int prev = 0;
            for (int j = 1; j <= n; j++) {
                int temp = dp[j];
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[j] = prev + 1;
                } else {
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                prev = temp;
            }
        }
        return dp[n];
    }
}

二、72. 编辑距离(中等)

题目描述

给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数。 你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

核心思路:二维动态规划

这道题是字符串 DP 的 "终极模板",核心是定义状态并枚举三种操作的代价:

  • 定义 dp[i][j] 表示将 word1[0..i-1] 转换为 word2[0..j-1] 的最少操作数。
  • 状态转移:
    • 如果 word1[i-1] == word2[j-1]dp[i][j] = dp[i-1][j-1](字符相同,无需操作)
    • 如果不相等:dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
      • dp[i-1][j]:删除 word1 的第 i 个字符
      • dp[i][j-1]:在 word1 中插入 word2 的第 j 个字符
      • dp[i-1][j-1]:将 word1 的第 i 个字符替换为 word2 的第 j 个字符
  • 边界条件:dp[i][0] = i(将 word1 转为空串,需要删除 i 次),dp[0][j] = j(将空串转为 word2,需要插入 j 次)

完整代码(Java)

java

运行

复制代码
class Solution {
    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] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

优化:一维 DP

同样可以用滚动数组优化空间复杂度到 O (min (m,n)),但需要额外变量保存上一轮的值:

java

运行

复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        if (word1.length() < word2.length()) {
            return minDistance(word2, word1);
        }
        int m = word1.length();
        int n = word2.length();
        int[] dp = new int[n + 1];
        for (int j = 0; j <= n; j++) {
            dp[j] = j;
        }
        for (int i = 1; i <= m; i++) {
            int prev = dp[0];
            dp[0] = i;
            for (int j = 1; j <= n; j++) {
                int temp = dp[j];
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[j] = prev;
                } else {
                    dp[j] = 1 + Math.min(Math.min(dp[j], dp[j - 1]), prev);
                }
                prev = temp;
            }
        }
        return dp[n];
    }
}

两道题对比总结

表格

题目 核心思想 时间复杂度 空间复杂度 关键考点
最长公共子序列 匹配 / 不匹配的状态转移 O(mn) O (min (m,n))(优化后) 双字符串 DP、滚动数组优化
编辑距离 三种操作的代价枚举 O(mn) O (min (m,n))(优化后) 操作定义、状态转移的边界条件

这两道题是字符串动态规划的 "标杆",它们的状态定义和转移方式几乎是所有字符串 DP 问题的原型,掌握它们就能举一反三解决大部分类似题目。

相关推荐
To_OC3 天前
LC 207 课程表:刚学图论那会儿,我连这是拓扑排序都没看出来
javascript·算法·leetcode
To_OC3 天前
LC 208 实现 Trie 前缀树:曾被名字劝退,写完发现是送分题
javascript·算法·leetcode
To_OC4 天前
LC 994 腐烂的橘子:人人都说是 BFS 入门题,我却写了三遍才过
javascript·算法·leetcode
To_OC4 天前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
To_OC5 天前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
To_OC7 天前
LC 49 字母异位词分组:想到哈希表很简单,选对 key 才是精髓
javascript·算法·leetcode
To_OC8 天前
LC 1 两数之和:面试第一道必考题,暴力解法直接被面试官 pass
javascript·算法·leetcode
想吃火锅100515 天前
【leetcode】121.买卖股票的最佳时机js/c++
算法·leetcode·职场和发展
凌波粒15 天前
LeetCode--491.递增子序列(回溯算法)
数据结构·算法·leetcode
退休倒计时15 天前
【每日一题】LeetCode 146. LRU 缓存 TypeScript
算法·leetcode·缓存·typescript