72. 编辑距离
考点
- LCS
思路
1. 问题定义
给定两个字符串 \(A=\texttt{word1}\)、\(B=\texttt{word2}\)。允许三种操作,每次操作代价为 1:
- 插入一个字符(Insert)
- 删除一个字符(Delete)
- 替换一个字符(Replace)
目标:求将 \(A\) 转换为 \(B\) 的最小操作次数,即编辑距离(Levenshtein distance)。
2. 状态设计(二维前缀 DP)
设 \(n_1 = |A|\),\(n_2 = |B|\)。
定义状态: \[ f[i][j] = \text{将 } A[0..i-1] \text{ 转换成 } B[0..j-1] \text{ 的最小操作数} \] 其中 \(i \in [0,n_1]\),\(j \in [0,n_2]\)。 该定义的关键点在于:状态描述的是两个前缀已经完全对齐后的最优代价(完成态),而不是某个“中间字符串”的真实形态。
3. 初始值(边界条件)
3.1 空串到空串
\[ f[0][0] = 0 \]
3.2 将非空前缀变为空串:只能删除
\[ f[i][0] = i \quad (1 \le i \le n_1) \]
因为把 \(A[0..i-1]\) 变成空串只能删除全部 \(i\) 个字符。
3.3 将空串变为非空前缀:只能插入
\[ f[0][j] = j \quad (1 \le j \le n_2) \]
因为从空串得到 \(B[0..j-1]\) 只能插入 \(j\) 个字符。
4. 状态转移方程(核心)
考虑 \(f[i][j]\)(\(i\ge 1, j\ge 1\)),只需要关注最后一位字符:
- \(A[i-1]\) —— 当前 \(A\) 前缀的最后字符
- \(B[j-1]\) —— 当前 \(B\) 前缀的最后字符
4.1 若最后字符相等
若 \(A[i-1] = B[j-1]\),最后一位无需操作: \[ f[i][j] = f[i-1][j-1] \]
4.2 若最后字符不相等
若 \(A[i-1] \ne B[j-1]\),最后一步必须执行一次操作(+1),且只有三种选择:
删除 \(A[i-1]\): 将 \(A[0..i-2]\) 变为 \(B[0..j-1]\) \[ f[i][j] \leftarrow f[i-1][j] + 1 \]
插入 \(B[j-1]\): 将 \(A[0..i-1]\) 变为 \(B[0..j-2]\),再插入一个字符使末位对齐 \[ f[i][j] \leftarrow f[i][j-1] + 1 \]
替换 \(A[i-1]\to B[j-1]\): 将 \(A[0..i-2]\) 变为 \(B[0..j-2]\),再替换末位 \[ f[i][j] \leftarrow f[i-1][j-1] + 1 \]
综合取最小值: \[ f[i][j] = 1 + \min\bigl(f[i-1][j],\ f[i][j-1],\ f[i-1][j-1]\bigr) \quad \text{当 } A[i-1]\ne B[j-1] \]
5. 最终答案位置
完整字符串对应的前缀是 \(A[0..n_1-1]\)、\(B[0..n_2-1]\),因此答案为: \[ \boxed{f[n_1][n_2]} \]
6. 为什么“插入”在 DP 里只能用“从后面插”来建模?
这一点的关键在于:状态 \(f[i][j]\) 的语义是“两个前缀完全对齐后的最优代价”。因此每一次转移都必须满足一个不变式:
从某个前缀对齐态转移到另一个前缀对齐态时, 下标 \(i, j\) 的变化必须精确对应“处理了哪些前缀字符”。
6.1 “插入”为什么对应 \(f[i][j-1] + 1\)?
若把最后一步设为“插入”,其含义是: 为了得到目标前缀 \(B[0..j-1]\),最后一个字符 \(B[j-1]\) 是新增出来的。 则插入之前,目标应当还只有 \(B[0..j-2]\),源仍为 \(A[0..i-1]\): \[ A[0..i-1] \to B[0..j-2] \quad \text{然后插入 } B[j-1] \] 因此插入的前驱状态必然是 \(f[i][j-1]\)。
6.2 为什么不能把“前面插入”当成独立转移?
若试图把操作解释为“在前面插入一个字符”,那么会导致源串前缀的字符整体右移: 原来第 \(k\) 个字符的相对位置被改变,进而出现“已经对齐的前缀被打乱”的现象。
在 \(f[i][j]\) 的语义下,前缀 \(A[0..i-1]\) 被视为一个固定集合(并以其原始顺序为准)。 “前插”会在过程层面改变对齐关系,从而产生一种难以用 \((i,j)\) 表示的“中间态”: 它既不是 \(A\) 的前 \(i\) 个字符,也不是前 \(i+1\) 个字符的对齐完成态。
换言之:
- DP 转移要求:前驱状态必须仍然是一个合法的前缀状态
- “前面插入”会破坏“前缀对齐已完成”的不变式,无法映射为单一的 \(f[\cdot][\cdot]\) 状态
因此,在该前缀 DP 建模下,“插入”只能以“补齐目标前缀的最后一个字符”的形式出现,也即: \[ f[i][j] \leftarrow f[i][j-1] + 1 \] 这并非对真实操作位置的限制,而是对状态建模一致性的限制:只有“末尾补齐”才能保持 dp 下标语义不被破坏。
7. 复杂度
- 二维 DP:时间 \(O(n_1 n_2)\),空间 \(O(n_1 n_2)\)
- 滚动数组:时间 \(O(n_1 n_2)\),空间 \(O(n_2)\)
8. 参考代码与注释
1 | class Solution { |
滚动数组:
1 | class Solution { |