LeetCode 72. 编辑距离(动态规划)
一、完整代码(精简注释版)
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
// dp[i][j]:word1前i个字符转换成word2前j个字符的最少操作次数
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
// 初始化:word2为空,需要删除word1的所有字符
for(int i=0; i<=m; i++){
dp[i][0] = i;
}
// 初始化:word1为空,需要插入word2的所有字符
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[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1]; // 字符相同,无需操作
}
else{
dp[i][j] = min({
dp[i-1][j]+1, // 删除
dp[i][j-1]+1, // 插入
dp[i-1][j-1]+1 // 替换
});
}
}
}
return dp[m][n];
}
};
二、动态规划五步总结
1. 确定 dp 数组的含义
dp[i][j] 表示:将 word1 的前 i 个字符转换成 word2 的前 j 个字符,所需要的最少操作次数。
例如:
word1 = "horse"
word2 = "ros"
dp[1][1]:将 "h" 转换成 "r" 的最少操作次数
dp[2][1]:将 "ho" 转换成 "r" 的最少操作次数
dp[5][3]:将 "horse" 转换成 "ros" 的最少操作次数
注意:DP 数组下标表示字符数量,所以实际比较字符时需要使用 word1[i-1] 和 word2[j-1]。
2. 确定状态转移方程(重点)
分为两种情况。
情况一:当前两个字符相同
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
因为当前字符相同,不需要额外操作,直接继承左上角的状态。
情况二:当前两个字符不同
可以选择删除、插入、替换三种操作:
dp[i][j] = min({
dp[i-1][j]+1, // 删除
dp[i][j-1]+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 当前字符 |
其中 +1 表示当前执行一次编辑操作。
min() 表示三种方案只选择操作次数最少的一种,而不是执行全部三种操作。
3. 初始化 dp 数组
for(int i=0; i<=m; i++){
dp[i][0] = i;
}
for(int j=0; j<=n; j++){
dp[0][j] = j;
}
-
dp[i][0]=i:word2 为空,需要将 word1 的前 i 个字符全部删除。 -
dp[0][j]=j:word1 为空,需要插入 j 个字符。 -
dp[0][0]=0:两个字符串都为空,不需要操作。
4. 确定遍历顺序
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
// 状态转移
}
}
因为当前状态 dp[i][j] 依赖三个已经计算的状态:
dp[i-1][j-1] 左上方
dp[i-1][j] 上方
dp[i][j-1] 左方
因此从上到下、从左到右遍历即可。
5. 返回最终结果
return dp[m][n];
因为 m、n 分别表示两个字符串的完整长度,所以 dp[m][n] 就是将整个 word1 转换为 word2 的最少操作次数。
三、举例模拟
word1 = "ab"
word2 = "ac"
初始化并计算完成后:
| 空 | a | c | |
|---|---|---|---|
| 空 | 0 | 1 | 2 |
| a | 1 | 0 | 1 |
| b | 2 | 1 | 1 |
计算 dp[2][2]:
由于 b != c,因此:
dp[2][2] = min({
dp[1][2]+1, // 2+1=3,删除
dp[2][1]+1, // 1+1=2,插入
dp[1][1]+1 // 0+1=1,替换
});
最终:
dp[2][2] = 1;
因为只需要将 b 替换成 c,就能把 "ab" 转换成 "ac"。
四、复杂度分析
时间复杂度: \(O(mn)\)
两层循环遍历整个二维 DP 数组。
空间复杂度: \(O(mn)\)
需要创建大小为 (m+1) × (n+1) 的二维数组。
五、重点记忆
-
DP 含义:
dp[i][j]表示两个字符串前缀之间的最少编辑次数。 -
字符相同:直接继承左上角
dp[i-1][j-1]。 -
字符不同:删除、插入、替换三种操作取最小值。
-
初始化:
dp[i][0]=i,dp[0][j]=j。 -
遍历范围:从 1 开始,使用
<=m和<=n,最后返回dp[m][n]。
核心代码模板:
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else{
dp[i][j] = min({
dp[i-1][j]+1,
dp[i][j-1]+1,
dp[i-1][j-1]+1
});
}
另外,C++ 中:
min(a, b); // 两个数取最小值
min({a, b, c}); // 三个数取最小值
max({a, b, c}); // 三个数取最大值
一句话总结:编辑距离就是利用二维 DP,当前字符相同则不增加操作次数,不同则从删除、插入、替换三种方案中选择代价最小的方案。