前言
今天复盘两道字符串动态规划的 "天花板级模板题"------「最长公共子序列」和「编辑距离」。它们是双字符串 DP 的经典代表,掌握了这两道题的解法,大部分字符串 DP 问题都能找到思路。
一、1143. 最长公共子序列(中等)
题目描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 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] = 0,dp[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. 编辑距离(中等)
题目描述
给你两个单词 word1 和 word2,请你计算出将 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 问题的原型,掌握它们就能举一反三解决大部分类似题目。