【力扣100题】55.编辑距离

题目描述

给你两个单词 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')

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 备注
暴力搜索 枚举所有操作 O(3^(n+m)) O(n+m) 会超时
记忆化搜索 递归 + 备忘录 O(n*m) O(n*m) 剪枝优化
动态规划 二维 dp 填表 O(n*m) O(n*m) 最常用
空间优化 一维数组滚动 O(n*m) O(min(n,m)) 面试优化

一、动态规划(最常用)

核心思想

定义 dp[i][j] = 将 word1[0...i-1] 转换成 word2[0...j-1] 的最少操作数。

状态转移方程

复制代码
如果 word1[i-1] == word2[j-1]:
    dp[i][j] = dp[i-1][j-1]  // 最后一个字符相等,不需要操作

否则(三选一,取最小):
    替换:dp[i-1][j-1] + 1  // 把 word1[i-1] 替换成 word2[j-1]
    删除:dp[i-1][j] + 1    // 删除 word1[i-1]
    插入:dp[i][j-1] + 1    // 在 word1 末尾插入 word2[j-1]

    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1

图解

复制代码
word1 = "horse", word2 = "ros"

dp 表(n+1 x m+1,0行0列为空字符串的情况):

       ""   r    o    s
""      0   1    2    3      <- 将空串变成 "ros" 需要插入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

dp[5][3] = 3 即为答案

代码实现

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.size(), m = word2.size();

        // dp[i][j] = 将 word1[0..i-1] 转换成 word2[0..j-1] 的最少操作数
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

        // 初始化:空串变成 word2[0..j-1] 需要 j 次插入
        for (int i = 0; i <= n; i++) dp[i][0] = i;

        // 初始化:word1[0..i-1] 变成空串需要 i 次删除
        for (int j = 0; j <= m; j++) dp[0][j] = j;

        // 填表
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    // 最后一个字符相等,不需要操作
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 三选一:替换、删除、插入
                    dp[i][j] = min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
                }
            }
        }

        return dp[n][m];
    }
};

二、算法流程图

详细填表过程

复制代码
word1 = "horse", word2 = "ros"

初始化 dp 表:
       ""   r    o    s
""      0   1    2    3
h       1
o       2
r       3
s       4
e       5

填表(从 i=1, j=1 开始):

(1,1): word1[0]='h', word2[0]='r' -> 不等
       替换: dp[0][0]+1=1
       删除: dp[0][1]+1=2
       插入: dp[1][0]+1=2
       dp[1][1] = min(1,2,2)+1 = 2
       ""   r    o    s
""      0   1    2    3
h       1   2

(1,2): word1[0]='h', word2[1]='o' -> 不等
       dp[1][2] = min(dp[0][1], dp[0][2], dp[1][1]) + 1 = min(1,2,2)+1 = 2

(1,3): word1[0]='h', word2[2]='s' -> 不等
       dp[1][3] = min(dp[0][2], dp[0][3], dp[1][2]) + 1 = min(2,3,2)+1 = 3

继续填满整个表:

       ""   r    o    s
""      0   1    2    3
h       1   2    2    3
o       2   2    2    3
r       3   2    3    3
s       4   3    4    3
e       5   4    5    4

答案:dp[5][3] = 4? 不对,应该是 3

重新计算:
dp[5][3] = min(dp[4][2], dp[4][3], dp[5][2]) + 1 = min(2,3,4) + 1 = 3  ✓

路径追溯:
horse -> rorse (替换 'h' -> 'r')
rorse -> rose  (删除 'r')
rose -> ros    (删除 'e')

三、逐行解析(对照原题代码)

cpp 复制代码
int minDistance(string word1, string word2) {
    int n = word1.size(), m = word2.size();

    // dp 大小为 (n+1) x (m+1)
    // dp[i][j] = 将 word1[0..i-1] 转换成 word2[0..j-1] 的最少操作数
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

    // 初始化第一列:word1[0..i-1] 变成空串,需要 i 次删除
    // horse -> "" : 删除 h, o, r, s, e = 5 次
    for (int i = 0; i <= n; i++) dp[i][0] = i;

    // 初始化第一行:空串变成 word2[0..j-1],需要 j 次插入
    // "" -> ros : 插入 r, o, s = 3 次
    for (int j = 0; j <= m; j++) dp[0][j] = j;

    // 填表
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (word1[i - 1] == word2[j - 1]) {
                // 两个字符相等,不需要任何操作
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 三种操作取最小:
                // 1. 替换:dp[i-1][j-1] + 1(把 word1[i-1] 替换成 word2[j-1])
                // 2. 删除:dp[i-1][j] + 1(删除 word1[i-1])
                // 3. 插入:dp[i][j-1] + 1(在 word1 末尾插入 word2[j-1])
                dp[i][j] = min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
            }
        }
    }

    return dp[n][m];
}

关键点解释

语句 含义
dp[i][0] = i 将 word1[0...i-1] 变成空串,需要删除 i 个字符
dp[0][j] = j 将空串变成 word2[0...j-1],需要插入 j 个字符
dp[i-1][j-1] + 1(替换) word1[i-1] != word2[j-1],把 word1[i-1] 替换成 word2[j-1]
dp[i-1][j] + 1(删除) 把 word1[i-1] 删除,word1 缩短为 [0...i-2]
dp[i][j-1] + 1(插入) 在 word1 末尾插入 word2[j-1],word2 缩短为 [0...j-2]

四、三种操作图解

复制代码
dp[i][j] 的三种推导:

1. 替换 (word1[i-1] -> word2[j-1])
   word1[0..i-2] -> word2[0..j-2] (dp[i-1][j-1] 步)
   再把 word1[i-1] 替换成 word2[j-1] (1 步)
   = dp[i-1][j-1] + 1

2. 删除 (删除 word1[i-1])
   word1[0..i-2] -> word2[0..j-1] (dp[i-1][j] 步)
   再删除 word1[i-1] (1 步)
   = dp[i-1][j] + 1

3. 插入 (在 word1 末尾插入 word2[j-1])
   word1[0..i-1] -> word2[0..j-2] (dp[i][j-1] 步)
   再插入 word2[j-1] (1 步)
   = dp[i][j-1] + 1

五、记忆化搜索(递归版)

思路

从后往前递归,字符相等则跳过,否则尝试三种操作取最小。

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.size(), m = word2.size();
        vector<vector<int>> memo(n, vector<int>(m, -1));
        return dfs(n - 1, m - 1, word1, word2, memo);
    }

private:
    int dfs(int i, int j, string& word1, string& word2, vector<vector<int>>& memo) {
        if (i < 0) return j + 1;  // word1 为空,需要 j+1 次插入
        if (j < 0) return i + 1;  // word2 为空,需要 i+1 次删除
        if (memo[i][j] != -1) return memo[i][j];

        if (word1[i] == word2[j]) {
            memo[i][j] = dfs(i - 1, j - 1, word1, word2, memo);
        } else {
            memo[i][j] = min(min(dfs(i - 1, j - 1, word1, word2, memo),  // 替换
                               dfs(i - 1, j, word1, word2, memo)),         // 删除
                               dfs(i, j - 1, word1, word2, memo))         // 插入
                        + 1;
        }
        return memo[i][j];
    }
};

六、与第1143题(最长公共子序列)对比

维度 第1143题 LCS 第72题 编辑距离
问题类型 求最长公共子序列长度 求最小编辑距离
操作 只涉及"保留" 插入、删除、替换
dp 含义 匹配的最大数量 最少操作次数
转移方程(匹配) dp[i][j] = dp[i-1][j-1] + 1 dp[i][j] = dp[i-1][j-1]
转移方程(不匹配) dp[i][j] = max(dp[i-1][j], dp[i][j-1]) dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1

复杂度分析

方法 时间复杂度 空间复杂度 备注
暴力搜索 O(3^(n+m)) O(n+m) 会超时
记忆化搜索 O(n*m) O(n*m) 剪枝优化
动态规划 O(n*m) O(n*m) 最常用
空间优化 O(n*m) O(min(n,m)) 较复杂

面试追问 FAQ

问题 回答要点
Q: 为什么有三种操作而不是两种? 插入和删除是不同的:插入是在 word1 中添加字符,删除是从 word1 中移除字符。替换可以看作删除+插入,但更高效
Q: 能否把插入和删除合并? 不能,因为编辑距离是要把 word1 变成 word2,两种操作的含义不同
Q: 为什么 dp[i-1][j-1] 是替换而不是删除+插入? 替换一步到位,而删除+插入需要两步,所以取 min 时替换优先
Q: 编辑距离的应用场景? 拼写检查、DNA 序列比对、机器翻译、代码差异提示等
Q: 时间复杂度能否优化? 不能,编辑距离问题是 NPC 问题,目前已知最优是 O(n*m)

相关题目

题目编号 题目名称 难度 核心差异
72 编辑距离 困难 基础题,三种操作
1143 最长公共子序列 中等 LCS,只保留不匹配
10 正则表达式匹配 困难 DP + 通配符
44 通配符匹配 困难 DP + ?/*
583 两个字符串的删除操作 中等 只有删除操作
712 两个字符串的最小 ASCII 删除和 中等 删除求最小 ASCII 码和

总结

要点 内容
核心思想 动态规划,word1 和 word2 两个维度
状态定义 dp[i][j] = 将 word1[0...i-1] 转换成 word2[0...j-1] 的最少操作数
转移方程 相等: dp[i-1][j-1];不等: min(替换,删除,插入) + 1
初始化 dp[i][0] = i(删除),dp[0][j] = j(插入)
空间优化 一维数组较复杂,建议用二维
与LCS对比 LCS 求最大匹配,编辑距离求最小操作

编辑距离是经典的字符串 DP 问题,难度比 LCS 更高,需要深入理解三种操作的含义和选择。


相关推荐
洛水水1 小时前
【力扣100题】62.滑动窗口最大值
数据结构·算法·leetcode
IronMurphy1 小时前
算法五十一 64. 最小路径和
算法
醒醒该学习了!1 小时前
Prompt提示词——带有深度思考模型的提示方法(理论篇)
人工智能·算法·prompt
君为先-bey1 小时前
Latte——视频生成的潜在扩散变换器
算法·机器学习·音视频·扩散模型
浅念-2 小时前
LeetCode刷题专题:FloodFill泛滥填充算法剖析
数据结构·算法·leetcode·职场和发展·深度优先·宽度优先
笨蛋不要掉眼泪2 小时前
Java并发编程:深入剖析 ArrayBlockingQueue
java·开发语言·算法·并发
菜菜的顾清寒2 小时前
力扣HOT100(33)二叉树的最大深度
算法·leetcode·职场和发展
Deepoch2 小时前
Deepoc数学大模型:重塑半导体研发与制造的核心算法范式
人工智能·算法·机器学习·半导体·deepoc·数学大模型
一支黑色の铅笔2 小时前
MongoDB Aggregation Pipeline 常用 Stage 速查
数据库·算法·mongodb