LeetCode 第72题:编辑距离
题目描述
给你两个单词 word1 和 word2,请返回将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
难度
困难
题目链接
示例
示例 1:
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
提示
0 <= word1.length, word2.length <= 500word1和word2由小写英文字母组成
解题思路
动态规划
这道题是经典的编辑距离问题,最适合使用动态规划来解决。
关键点:
- 定义状态:
dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最少操作数 - 状态转移方程:
- 如果
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-1][j], dp[i][j-1]) + 1dp[i-1][j-1] + 1表示替换操作dp[i-1][j] + 1表示删除操作dp[i][j-1] + 1表示插入操作
- 如果
- 初始化:
dp[i][0] = i(将word1的前i个字符全部删除)dp[0][j] = j(插入word2的前j个字符)
- 最终结果:
dp[m][n],其中m和n分别是word1和word2的长度
具体步骤:
- 初始化
dp数组 - 填充
dp数组 - 返回
dp[m][n]
图解思路
算法步骤分析表
以 word1 = "horse", word2 = "ros" 为例:
| 步骤 | 操作 | dp数组 | 说明 |
|---|---|---|---|
| 初始 | 初始化 | dp[0][0]=0, dp[i][0]=i, dp[0][j]=j | 边界情况 |
| 遍历 | (1,1) | dp[1][1]=1 | h!=r,取min(0,1,1)+1=1 |
| 遍历 | (1,2) | dp[1][2]=2 | h!=o,取min(1,1,2)+1=2 |
| 遍历 | (1,3) | dp[1][3]=3 | h!=s,取min(2,2,3)+1=3 |
| 遍历 | (2,1) | dp[2][1]=2 | o!=r,取min(1,2,1)+1=2 |
| 遍历 | (2,2) | dp[2][2]=1 | o==o,取dp[1][1]=1 |
| ... | ... | ... | ... |
| 最终 | 结果 | dp[5][3]=3 | 最少需要3步 |
状态/情况分析表
| 情况 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 相同字符串 | "abc", "abc" | 0 | 不需要任何操作 |
| 一个为空 | "", "abc" | 3 | 需要插入所有字符 |
| 完全不同 | "abc", "def" | 3 | 需要替换所有字符 |
| 部分相同 | "horse", "ros" | 3 | 需要多种操作组合 |
代码实现
C# 实现
public class Solution {
public int MinDistance(string word1, string word2) {
int m = word1.Length;
int n = word2.Length;
// 如果有一个字符串为空,则编辑距离为另一个字符串的长度
if (m == 0) return n;
if (n == 0) return m;
// 创建dp数组
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;
}
// 填充dp数组
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] = Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1])) + 1;
}
}
}
return dp[m, n];
}
}
Python 实现
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# 如果有一个字符串为空,则编辑距离为另一个字符串的长度
if m == 0:
return n
if n == 0:
return m
# 创建dp数组
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 填充dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
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 - 1][j], dp[i][j - 1]) + 1
return dp[m][n]
C++ 实现
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length();
int n = word2.length();
// 如果有一个字符串为空,则编辑距离为另一个字符串的长度
if (m == 0) return n;
if (n == 0) return m;
// 创建dp数组
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 初始化边界
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充dp数组
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], min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
}
}
return dp[m][n];
}
};
执行结果
- C# 执行用时:84 ms
- C# 内存消耗:39.2 MB
- Python 执行用时:160 ms
- Python 内存消耗:17.8 MB
- C++ 执行用时:12 ms
- C++ 内存消耗:9.1 MB
代码亮点
- 🎯 使用动态规划高效解决问题
- 💡 清晰的状态定义和转移方程
- 🔍 处理边界情况(空字符串)
- 🎨 代码结构清晰,易于理解
常见错误分析
- 🚫 状态转移方程错误
- 🚫 边界条件处理不当
- 🚫 没有考虑空字符串的情况
- 🚫 混淆插入和删除操作的含义
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归(暴力) | O(3^n) | O(n) | 直观易懂 | 效率极低,会超时 |
| 记忆化递归 | O(m*n) | O(m*n) | 相对简单 | 递归栈可能溢出 |
| 动态规划 | O(m*n) | O(m*n) | 高效稳定 | 需要额外空间 |
| 空间优化DP | O(m*n) | O(min(m,n)) | 节省空间 | 实现复杂 |