📌 题目链接:72. 编辑距离 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:字符串、动态规划
⏱️ 目标时间复杂度:O(mn)
💾 空间复杂度:O(mn)
🧠 题目分析
本题要求计算将一个字符串 word1 转换为另一个字符串 word2 所需的最少操作次数,允许的操作有:
- 插入一个字符
- 删除一个字符
- 替换一个字符
这类问题在自然语言处理(NLP)、拼写检查、生物信息学(如 DNA 序列比对)等领域有广泛应用。例如,在机器翻译系统中,编辑距离常被用作评估译文与参考译文相似程度的指标之一。
💡 关键观察:
虽然题目提供了三种操作,但通过等价转换可以发现:
- “删除 word1 中的一个字符” ≡ “在 word2 中插入一个字符”
- “在 word1 中插入一个字符” ≡ “删除 word2 中的一个字符”
- “替换 word1 的某个字符” ≡ “替换 word2 的对应字符”
因此,本质上只需考虑三种独立操作,并利用最优子结构和重叠子问题特性,采用动态规划(Dynamic Programming, DP) 求解。
🧩 核心算法及代码讲解
✅ 动态规划思想详解
我们定义状态 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数。
📌 边界条件(Base Cases)
- 当
word1为空(即i = 0),要变成长度为j的word2,只能执行j次插入 →dp[0][j] = j - 当
word2为空(即j = 0),要将长度为i的word1变为空,只能执行i次删除 →dp[i][0] = i
🔄 状态转移方程
对于 i ≥ 1 且 j ≥ 1,比较 word1[i-1] 与 word2[j-1](注意下标从 0 开始):
- 若字符相同:无需操作,直接继承
dp[i-1][j-1]dp[i][j] = dp[i-1][j-1]; - 若字符不同:取以下三种操作的最小值 +1:
- 删除
word1[i-1]→ 来自dp[i-1][j] + 1 - 插入
word2[j-1]到word1末尾 → 来自dp[i][j-1] + 1 - 替换
word1[i-1]为word2[j-1]→ 来自dp[i-1][j-1] + 1
- 删除
综上,统一写成:
int left = dp[i - 1][j] + 1; // 删除 word1[i-1]
int down = dp[i][j - 1] + 1; // 插入 word2[j-1]
int left_down = dp[i - 1][j - 1]; // 替换 or 不变
if (word1[i - 1] != word2[j - 1]) {
left_down += 1; // 字符不同才需要替换
}
dp[i][j] = min({left, down, left_down});
💡 为什么只看末尾字符?
因为 DP 是按前缀构建的,所有操作都可以“推迟”到最后一步进行而不影响结果(操作顺序可交换)。这是 DP 能成立的关键前提。
🧭 解题思路(分步拆解)
- 初始化 DP 表:创建
(n+1) x (m+1)的二维数组,其中n = word1.length(),m = word2.length()。 - 填充边界:
- 第一列为
0..n(全删除) - 第一行为
0..m(全插入)
- 第一列为
- 逐行/列填表:从
(1,1)开始,根据状态转移方程更新每个dp[i][j]。 - 返回结果:
dp[n][m]即为答案。
✅ 示例验证(word1="horse", word2="ros"):
| r | o | s | ||
|---|---|---|---|---|
| 0 | 1 | 2 | 3 | |
| h | 1 | 1 | 2 | 3 |
| o | 2 | 2 | 1 | 2 |
| r | 3 | 2 | 2 | 2 |
| s | 4 | 3 | 3 | 2 |
| e | 5 | 4 | 4 | 3 ← 结果 |
📊 算法分析
| 项目 | 内容 |
|---|---|
| 时间复杂度 | O(mn),双重循环遍历整个 DP 表 |
| 空间复杂度 | O(mn),存储 (n+1)x(m+1) 的 DP 表 |
| 优化方向 | 可压缩为 O(min(m,n)) 空间(滚动数组),但面试中通常先写出标准二维 DP) |
| 面试考点 | - 状态定义能力- 边界处理意识- 对“操作等价性”的理解- 能否解释为何 DP 有效 |
🎯 高频面试追问:
- 如果只允许插入和删除,怎么做?
- 如何输出具体的编辑操作序列(而不仅是次数)?
- 能否用记忆化搜索(递归+缓存)实现?
💻 代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.length();
int m = word2.length();
// 有一个字符串为空串
if (n * m == 0) return n + m;
// DP 数组
vector<vector<int>> D(n + 1, vector<int>(m + 1));
// 边界状态初始化
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1; // 删除 word1[i-1]
int down = D[i][j - 1] + 1; // 在 word1 末尾插入 word2[j-1]
int left_down = D[i - 1][j - 1]; // 替换或不变
if (word1[i - 1] != word2[j - 1])
left_down += 1; // 字符不同,需替换
D[i][j] = min(left, min(down, left_down));
}
}
return D[n][m];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 示例 1
cout << sol.minDistance("horse", "ros") << "\n"; // 输出: 3
// 示例 2
cout << sol.minDistance("intention", "execution") << "\n"; // 输出: 5
// 边界测试
cout << sol.minDistance("", "abc") << "\n"; // 输出: 3
cout << sol.minDistance("abc", "") << "\n"; // 输出: 3
cout << sol.minDistance("", "") << "\n"; // 输出: 0
return 0;
}
/**
* @param {string} word1
* @param {string} word2
* @return {number}
*/
var minDistance = function(word1, word2) {
const n = word1.length;
const m = word2.length;
if (n * m === 0) return n + m;
// 创建 (n+1) x (m+1) 的 DP 表
const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
// 初始化边界
for (let i = 0; i <= n; i++) dp[i][0] = i;
for (let j = 0; j <= m; j++) dp[0][j] = j;
// 填表
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
const left = dp[i - 1][j] + 1; // 删除
const down = dp[i][j - 1] + 1; // 插入
let leftDown = dp[i - 1][j - 1]; // 替换或不变
if (word1[i - 1] !== word2[j - 1]) {
leftDown += 1;
}
dp[i][j] = Math.min(left, down, leftDown);
}
}
return dp[n][m];
};
// 测试
console.log(minDistance("horse", "ros")); // 3
console.log(minDistance("intention", "execution")); // 5
console.log(minDistance("", "abc")); // 3
console.log(minDistance("abc", "")); // 3
console.log(minDistance("", "")); // 0
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!