中等
给你两个单词 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由小写英文字母组成
📝 核心笔记:编辑距离 (Edit Distance - DFS)
1. 核心思想 (一句话总结)
"改作业游戏:把单词 A 改成单词 B,每次只能动一个字母。如果当前字母不一样,我有三张牌可以出------'删除'、'插入'、'替换',看哪张牌能让我用最少的步数通关。"
- 状态定义 :
dfs(i, j)表示将s[0...i]变成t[0...j]所需的最少操作次数。 - 决策逻辑:
-
- 相等 :直接跳过,不做操作 (
dfs(i-1, j-1))。 - 不等 :尝试三种操作,取最小值
min(删, 插, 改) + 1。
- 相等 :直接跳过,不做操作 (
2. 算法流程 (DFS + Memo)
- 递归定义 (Recursion):
-
- 从两个字符串的末尾
(n-1, m-1)开始向前对比。
- 从两个字符串的末尾
- Base Case (字符串耗尽):
-
- 如果
i < 0(s 空了):要把 t 剩下的j+1个字符全部插入 进去,代价是j + 1。 - 如果
j < 0(t 空了):要把 s 剩下的i+1个字符全部删除 掉,代价是i + 1。
- 如果
- 状态转移 (Transition):
-
s[i] == t[j]:无需操作,继承dfs(i-1, j-1)。s[i] != t[j]:
-
-
- 删除 s[i]:
dfs(i-1, j)(s 变短了,t 没变)。 - 插入 (在 s 后插一个 t[j]) :
dfs(i, j-1)(s 指针不动,t 变短了/被匹配了)。 - 替换 ( s[i]变 **t[j]**) :
dfs(i-1, j-1)(两边都处理完了)。 - 取这三者的最小值,并
+1(当前操作的代价)。
- 删除 s[i]:
-
🔍 代码回忆清单
// 题目:LC 72. Edit Distance
class Solution {
private char[] s, t;
private int[][] memo;
public int minDistance(String text1, String text2) {
s = text1.toCharArray();
t = text2.toCharArray();
int n = s.length;
int m = t.length;
// memo[i][j] 记录 s的前i+1个 和 t的前j+1个 的最小编辑距离
memo = new int[n][m];
for (int[] row : memo) {
Arrays.fill(row, -1); // -1 表示未计算
}
return dfs(n - 1, m - 1);
}
private int dfs(int i, int j) {
// 1. Base Case: s 走完了,t 还没走完
// 意味着要把 t 剩余的前 j+1 个字符全部插入到 s
if (i < 0) {
return j + 1;
}
// 2. Base Case: t 走完了,s 还没走完
// 意味着要把 s 剩余的前 i+1 个字符全部删除
if (j < 0) {
return i + 1;
}
// 3. 查表
if (memo[i][j] != -1) {
return memo[i][j];
}
// 4. 字符匹配:不用任何操作,直接看前面的
if (s[i] == t[j]) {
return memo[i][j] = dfs(i - 1, j - 1);
}
// 5. 字符不匹配:三选一 + 1
// dfs(i - 1, j) -> 删除 s[i]
// dfs(i, j - 1) -> 插入 (相当于消掉了 t[j])
// dfs(i - 1, j - 1) -> 替换
return memo[i][j] = Math.min(
Math.min(dfs(i - 1, j), dfs(i, j - 1)),
dfs(i - 1, j - 1)
) + 1;
}
}
⚡ 快速复习 CheckList (易错点)
-
\] **为什么** **i < 0****返回** **j + 1****?**
-
i < 0说明s已经是空串了。如果t还有0到j共j+1个字符,那必须执行j+1次插入操作才能变过去。
-
\] **三个递归分别代表什么物理意义?**
-
dfs(i-1, j):删除。s 的这个字符不要了,跳过它。dfs(i, j-1):插入。假设我们在 s 后面插了一个和 t[j] 一样的字符,那么 t[j] 就被匹配掉了(j 往前移),但 s 原来的 i 还在等着被处理(i 不动)。dfs(i-1, j-1):替换。把 s[i] 强行改成 t[j],然后两人一起往前移。
-
\] **时间复杂度?**
-
- O(N \\times M)。每个格子只会被计算一次。
🖼️ 中文数字演练
s = "horse", t = "ros"
目标:求 dfs(4, 2) ('e' vs 's')
- 比较 'e' 和 's':不相等。
-
- 尝试 替换 :
dfs(3, 1)('s' vs 'o') + 1。 - 尝试 删除 :
dfs(3, 2)('s' vs 's') + 1。 - 尝试 插入 :
dfs(4, 1)('e' vs 'o') + 1。
- 尝试 替换 :
- 分支:尝试 删除 ( dfs(3, 2)****):
-
- 比较
s[3]('s') 和t[2]('s')。相等! - 直接继承
dfs(2, 1)('r' vs 'o')。
- 比较
- 分支:继续 dfs(2, 1)****('r' vs 'o'):不相等。
-
- 尝试 替换 :
dfs(1, 0)('o' vs 'r') + 1。 - ... (继续递归) ...
- 尝试 替换 :
- 最终路径:
-
horse->rorse(h换成r, +1)rorse->rose(删掉r, +1)rose->ros(删掉e, +1)- 总共 3 步。
最终结果: 3。
📝 核心笔记:编辑距离 (Edit Distance - DP Iterative) 递推
1. 核心思想 (一句话总结)
"进化论表格:从'空串'进化到'完整串',每一步都可以选择'删'(继承上面)、'插'(继承左边)或'改'(继承左上角),我们要选代价最小的那条路。"
- 状态定义 :
f[i+1][j+1]表示s[前i+1个]变成t[前j+1个]的最小操作数。 - 物理意义:
-
- 左边 ( f[i+1][j]****):插入(s 变 t,相当于 t 多了一个,从 t 少一个的状态插进来)。
- 上边 ( f[i][j+1]****):删除(s 变 t,s 多了一个,把它删掉回到上一个状态)。
- 左上 ( f[i][j]****):替换(或者匹配)。
2. 算法流程 (DP 迭代)
- 初始化 (Init):
-
- 第 0 行 :
s是空串,t有j个字符。变成它需要j次插入。所以f[0][j] = j。 - 第 0 列 :
t是空串,s有i个字符。变成它需要i次删除。所以f[i][0] = i。
- 第 0 行 :
- 填表 (Loop):
-
- 遍历
s的每个字符i(对应行i+1) 和t的每个字符j(对应列j+1)。
- 遍历
- 状态转移 (Transition):
-
- 字符相等 ( s[i] == t[j]****) :直接继承左上角
f[i][j],不花钱。 - 字符不等:必须操作。
- 字符相等 ( s[i] == t[j]****) :直接继承左上角
-
-
f[i][j+1](上) + 1 -> 删除。f[i+1][j](左) + 1 -> 插入。f[i][j](左上) + 1 -> 替换。- 取三者最小值。
-
- 结果 :返回右下角
f[n][m]。
🔍 代码回忆清单
// 题目:LC 72. Edit Distance
class Solution {
public int minDistance(String text1, String text2) {
char[] s = text1.toCharArray();
char[] t = text2.toCharArray();
int n = s.length;
int m = t.length;
// 1. DP 表:多开一行一列处理空串情况
int[][] f = new int[n + 1][m + 1];
// 2. 初始化第 0 行 (s为空,变成 t 需要一直插入)
for (int j = 0; j < m; j++) {
f[0][j + 1] = j + 1; // f[0][1]=1, f[0][2]=2...
}
for (int i = 0; i < n; i++) {
// 3. 初始化第 0 列 (t为空,s 需要一直删除)
f[i + 1][0] = i + 1;
for (int j = 0; j < m; j++) {
// 4. 核心转移
// 如果字符相同,不需要操作,直接继承"左上角"
// 如果不同,在 (删除、插入、替换) 中取最小 + 1
// f[i][j+1] (上) -> 删除 s[i]
// f[i+1][j] (左) -> 插入 t[j]
// f[i][j] (左上) -> 替换
f[i + 1][j + 1] = s[i] == t[j] ? f[i][j] :
Math.min(Math.min(f[i][j + 1], f[i + 1][j]), f[i][j]) + 1;
}
}
// 5. 返回右下角
return f[n][m];
}
}
⚡ 快速复习 CheckList (易错点)
-
\] **初始化千万别忘了!**
-
- 如果不初始化第一行和第一列,它们默认是 0,会导致逻辑错误(空串变非空串代价怎么可能是 0?)。
-
\] **f[i][j+1]****到底是删还是插?**
-
i对应s,j对应t。f[i][j+1]意味着s少了一个字符(i比i+1少),t没变。- 既然
s少一点就能变过去,说明现在的s多了一个,所以是 删除。
-
\] **空间优化?**
-
- 虽然是二维,但每次只用到"上一行"和"左边"。
- 可以用一维数组(滚动数组)优化到 。需要用一个
temp变量记录左上角的值(pre)。
🖼️ 数字演练
s = "ros", t = "horse"
(注意:这里交换一下题目常见的例子,s变t)
DP 表片段:
- Row 0 (s="") :
[0, 1, 2, 3, 4, 5](变成 "h", "ho", "hor"...) - Row 1 (s="r"):
-
f[1][0] = 1(删r变空).- vs 'h' (j=0) : 'r'!='h'。
min(上=1, 左=1, 左上=0) + 1 = 1(替换)。 - vs 'o' (j=1) : 'r'!='o'。
min(上=2, 左=1, 左上=1) + 1 = 2。 - vs 'r' (j=2) : 'r'=='r'。Match!
f[1][3] = f[0][2] = 2("ho" 的代价)。
- Row 2 (s="o"):
-
- ...
- vs 'o' (j=1) : 'o'=='o'。Match!
f[2][2] = f[1][1] = 1。 - 这里的 1 来源于 "r" 变 "h" (1代价) -> "ro" 变 "ho" (依然1代价)。
最终结果: 3 (ros -> horse: r换h, 加r, 加e? 不对,是 horse -> ros = 3。这里 s=ros, t=horse 需要 3+2=5步? 不,编辑距离是对称的,3步)。