跳到主要内容

0072. 编辑距离

题目描述​

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

解题思路​

第一步:理解问题本质​

编辑距离(Edit Distance / Levenshtein Distance):两个字符串之间,由一个转成另一个所需的最少编辑操作次数。

核心思想:比较两个字符串的每个位置,决定需要哪种操作。

示例推演:"horse" -> "ros"

  • 对齐两个字符串:
    h o r s e
    r o s
  • 发现第1个字符不同,第2个相同,后面也错开了

第二步:递归思路(暴力解法)​

定义 dfs(i, j) = word1[0..i] 转换成 word2[0..j] 的最小编辑距离。

递归终止条件:

  • i < 0:word1 已用完,需要插入 j+1 个字符
  • j < 0:word2 已用完,需要删除 i+1 个字符

递归情况:

  • 如果 word1[i] == word2[j]:不用操作,dfs(i, j) = dfs(i-1, j-1)
  • 否则,取三种操作的最小值:
    • 删除:dfs(i-1, j) + 1(删除 word1[i])
    • 插入:dfs(i, j-1) + 1(在 word1[i] 后插入 word2[j])
    • 替换:dfs(i-1, j-1) + 1(将 word1[i] 替换为 word2[j])

问题:有大量重复计算,时间复杂度 O(3ⁿ)。

第三步:记忆化搜索​

用 @cache 装饰器缓存递归结果,避免重复计算。

# 同一组 (i, j) 会从多条路径到达,缓存后每个状态只算一次
@cache
def dfs(i: int, j: int) -> int:
# word1 用完,只能把 word2 剩下的 j+1 个字符全插进去
if i < 0: return j + 1
# word2 用完,只能把 word1 剩下的 i+1 个字符全删掉
if j < 0: return i + 1
# 末尾字符相同,这一步不花代价,直接对齐到前一位
if s[i] == t[j]:
return dfs(i - 1, j - 1)
# 删除 s[i]、插入 t[j]、把 s[i] 换成 t[j],三选一后代价加 1
return min(dfs(i - 1, j), dfs(i, j - 1), dfs(i - 1, j - 1)) + 1

第四步:动态规划​

将递归改为递推,定义 dp[i][j]:

  • word1 的前 i 个字符(word1[0..i-1])
  • 转换成 word2 的前 j 个字符(word2[0..j-1])
  • 所需的最小编辑距离

为什么下标要+1? 预留 dp[0][j] 和 dp[i][0] 处理空字符串:

  • dp[0][j] = j:空串变成 j 个字符,需要插入 j 次
  • dp[i][0] = i:i 个字符变成空串,需要删除 i 次

第五步:空间优化​

观察:dp[i][j] 只依赖于上一行和当前行的左边。

  • dp[i-1][j]:上一行
  • dp[i][j-1]:当前行左边
  • dp[i-1][j-1]:上一行左边(对角线)

滚动数组:只需保存两行,空间复杂度 O(m)。

一维数组:用一个数组 + 一个变量保存对角线,空间复杂度 O(m)。


完整代码实现​

算法可视化 · 逐帧回放这道题的代码142 帧 · 录制输入 "horse", "ros"
from functools import cache

class Solution:
def minDistance(self, word1: str, word2: str) -> int:
"""
编辑距离 - 动态规划(二维数组版)

dp[i][j] = word1[:i] 转换成 word2[:j] 的最小编辑距离
"""
n, m = len(word1), len(word2)

# dp[i][j] 表示 word1[:i] 转换为 word2[:j] 的最小编辑距离
dp = [[0] * (m + 1) for _ in range(n + 1)]

# 初始化边界
dp[0] = list(range(m + 1)) # dp[0][j] = j
for i in range(1, n + 1):
dp[i][0] = i # dp[i][0] = i

# 填充DP表
for i in range(1, n + 1):
for j in range(1, m + 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], # 删除
dp[i][j - 1], # 插入
dp[i - 1][j - 1] # 替换
) + 1

return dp[n][m]

def minDistanceOneArray(self, word1: str, word2: str) -> int:
"""
编辑距离 - 空间优化(一维数组版)
"""
m = len(word2)
f = list(range(m + 1))

for x in word1:
pre = f[0] # 保存对角线值
f[0] += 1

for j, y in enumerate(word2):
tmp = f[j + 1]
f[j + 1] = pre if x == y else min(f[j + 1], f[j], pre) + 1
pre = tmp

return f[m]

示例推演​

示例:word1 = "horse", word2 = "ros"

DP表(部分):

ros
0123
h1123
o2212
r3222
s4332
e5443

关键计算:

  • dp[1][1]:h vs r,不同 → min(1,1,0)+1 = 1(替换)
  • dp[2][2]:o vs o,相同 → dp[1][1] = 1
  • dp[5][3]:e vs s,不同 → min(3,4,2)+1 = 3

结果:dp[5][3] = 3


复杂度分析​

解法时间复杂度空间复杂度说明
暴力递归O(3ⁿ)O(n+m)有大量重复计算
记忆化搜索O(n×m)O(n×m)递归栈空间
二维DPO(n×m)O(n×m)标准解法
滚动数组O(n×m)O(m)空间优化
一维数组O(n×m)O(m)最优空间

易错点总结​

1. 下标对应关系​

# dp 多留了一行一列表示空串,所以长度 i 对应最后一个字符 word1[i-1]
# dp[i][j] 对应 word1[i-1] 和 word2[j-1]
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]

注意 dp 数组和字符串下标的偏移。

2. 边界初始化​

dp[0] = list(range(m + 1))  # dp[0][j] = j
for i in range(1, n + 1):
dp[i][0] = i # dp[i][0] = i

不要漏了 dp[i][0] 的初始化。

3. 空间优化的对角线保存​

pre = f[0]  # 先保存对角线:上一行 f[0],即 dp[i-1][0]
f[0] += 1 # 再更新本行 f[0](第一列只能删,长度即 i)
for j, y in enumerate(word2):
tmp = f[j + 1] # 保存当前值,它就是下一格要用的对角线
# 计算 f[j+1]
pre = tmp # 更新对角线供下一轮使用

4. 三种操作的理解​

操作dp转移实际含义
删除dp[i-1][j]删除word1[i-1]
插入dp[i][j-1]在word1[i-1]后插入word2[j-1]
替换dp[i-1][j-1]将word1[i-1]替换为word2[j-1]

扩展思考​

1. 如果操作代价不同?​

例如:插入删除代价为1,替换代价为2。

dp[i][j] = min(
dp[i-1][j] + 1, # 删除
dp[i][j-1] + 1, # 插入
dp[i-1][j-1] + 2 # 替换代价为2
)

2. 如何输出具体的编辑操作?​

需要在DP过程中记录"来源",回溯得到操作序列。

3. 最长公共子序列(LCS)与编辑距离的关系​

编辑距离 = len(word1) + len(word2) - 2 × LCS长度

当只允许插入和删除时成立。


应用场景​

  1. 拼写检查:计算用户输入与词典单词的编辑距离
  2. DNA序列比对:生物信息学中计算基因相似度
  3. 版本控制:diff 算法计算文件差异
  4. 语音识别:评价识别结果与标准文本的差异

相关题目​

自测​

加载评论中...