72. 编辑距离

72. 编辑距离

中等

给你两个单词 word1word2请返回将 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 <= 500
  • word1word2 由小写英文字母组成

📝 核心笔记:编辑距离 (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)
  1. 递归定义 (Recursion)
    • 从两个字符串的末尾 (n-1, m-1) 开始向前对比。
  1. Base Case (字符串耗尽)
    • 如果 i < 0 (s 空了):要把 t 剩下的 j+1 个字符全部插入 进去,代价是 j + 1
    • 如果 j < 0 (t 空了):要把 s 剩下的 i+1 个字符全部删除 掉,代价是 i + 1
  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 (当前操作的代价)。
🔍 代码回忆清单
复制代码
// 题目: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 还有 0jj+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')

  1. 比较 'e' 和 's':不相等。
    • 尝试 替换dfs(3, 1) ('s' vs 'o') + 1。
    • 尝试 删除dfs(3, 2) ('s' vs 's') + 1。
    • 尝试 插入dfs(4, 1) ('e' vs 'o') + 1。
  1. 分支:尝试 删除 ( dfs(3, 2)****)
    • 比较 s[3] ('s') 和 t[2] ('s')。相等!
    • 直接继承 dfs(2, 1) ('r' vs 'o')。
  1. 分支:继续 dfs(2, 1)****('r' vs 'o'):不相等。
    • 尝试 替换dfs(1, 0) ('o' vs 'r') + 1。
    • ... (继续递归) ...
  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 迭代)
  1. 初始化 (Init)
    • 第 0 行s 是空串,tj 个字符。变成它需要 j 次插入。所以 f[0][j] = j
    • 第 0 列t 是空串,si 个字符。变成它需要 i 次删除。所以 f[i][0] = i
  1. 填表 (Loop)
    • 遍历 s 的每个字符 i (对应行 i+1) 和 t 的每个字符 j (对应列 j+1)。
  1. 状态转移 (Transition)
    • 字符相等 ( s[i] == t[j]****) :直接继承左上角 f[i][j],不花钱。
    • 字符不等:必须操作。
      • f[i][j+1] (上) + 1 -> 删除
      • f[i+1][j] (左) + 1 -> 插入
      • f[i][j] (左上) + 1 -> 替换
      • 取三者最小值。
  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 对应 sj 对应 t
    • f[i][j+1] 意味着 s 少了一个字符(ii+1 少),t 没变。
    • 既然 s 少一点就能变过去,说明现在的 s 多了一个,所以是 删除
  • \] **空间优化?**

    • 虽然是二维,但每次只用到"上一行"和"左边"。
    • 可以用一维数组(滚动数组)优化到 。需要用一个 temp 变量记录左上角的值(pre)。
🖼️ 数字演练

s = "ros", t = "horse"

(注意:这里交换一下题目常见的例子,s变t)

DP 表片段:

  1. Row 0 (s="") : [0, 1, 2, 3, 4, 5] (变成 "h", "ho", "hor"...)
  2. 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" 的代价)。
  1. 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步)。

相关推荐
juleskk2 小时前
3.15 复试训练
算法
j_xxx404_2 小时前
力扣:525.连续数组和1314.矩阵区域和(二维前缀和)
算法·leetcode·矩阵
23.2 小时前
【Java】Arrays工具类——数组操作终极指南
java·算法·面试
Sunsets_Red2 小时前
模意义下及同余的公式整理
c语言·c++·数学·算法·c#·数论·信息学竞赛
计算机安禾2 小时前
【C语言程序设计】第27篇:递归函数原理与实例分析
c语言·开发语言·数据结构·c++·算法·蓝桥杯·visual studio
Barkamin2 小时前
直接选择排序
数据结构
無限進步D2 小时前
C++ 万能头
开发语言·c++·算法·蓝桥杯·竞赛·万能头
qq_418101772 小时前
C++中的状态模式
开发语言·c++·算法
weixin_307779132 小时前
构建健壮的XML文档抓取与摘要流水线:Requests + urllib3.Retry + lxml 实践
xml·开发语言·python·算法·性能优化