编辑距离 图解题解
这道题到底在问什么
- 输入
- word1="horse", word2="ros"
- 输出
- 3
最优解:为什么这么做
一句话答案:LeetCode 72 编辑距离的经典解法是二维动态规划:dp[i][j] 表示把 word1 的前 i 个字符变成 word2 的前 j 个字符的最少操作数,末位字符相同就直接继承左上角 dp[i-1][j-1],不同则在删除、插入、替换三条来路里取最小再加一。时间 O(m·n)、空间 O(m·n),可滚动压缩到 O(n)。
编辑距离这道题在问什么
给两个单词 word1 和 word2,每一步可以对 word1 插入、删除或替换一个字符,问把 word1 完全变成 word2 最少要几步。比如把 horse 变成 ros 只需 3 步。难点在于操作序列的自由度太大:先删哪个、在哪插、要不要替换,组合起来是天文数字,逐一枚举操作序列不可行。
为什么想到用两个前缀做状态
换个视角,别盯着操作序列,盯着两个字符串的末尾。想把 word1 的前 i 个字符变成 word2 的前 j 个字符,最优方案里对最后一个位置的处理只有四种可能:两个末位字符本来就相同,直接对上、不花操作;或者删掉 word1 的末字符;或者在末尾插入 word2 的末字符;或者把 word1 的末字符替换成 word2 的末字符。
无论选哪种,剩下的部分都是一个更短前缀对更短前缀的同样问题。也就是说,「word1 前 i 位变 word2 前 j 位的最少步数」只依赖三个更小的子问题,与前面具体怎么变过来的无关——这就是无后效性,二维动态规划的地基。
dp 表怎么定义,首行首列为什么是 0 到 n
定义 dp[i][j] 为把 word1 的前 i 个字符变成 word2 的前 j 个字符的最少操作数,答案就是右下角 dp[m][n]。表比字符串各多一行一列,用来表达空串。
边界先铺好:dp[0][j] = j,空串要变出长度 j 的前缀,只能一个个插入,恰好 j 步;dp[i][0] = i,要把长度 i 的前缀变成空串,只能一个个删光。左上角 dp[0][0] = 0,空串变空串不用动。忘了这两条边界是本题最常见的错误,后面每一格都建立在它们之上。
三条来路为什么分别对应删、插、替换
填内部格 dp[i][j] 时,先比较 word1 第 i 个字符和 word2 第 j 个字符。相同时这一对天然对上,dp[i][j] = dp[i-1][j-1],直接抄左上角,一步不花——注意相同时不能加一,多加就把答案算大了。
不同时三选一取最小再加一:dp[i-1][j] + 1 是删——先把 word1 少末位的前缀变好,再删掉这个多余字符;dp[i][j-1] + 1 是插——先变出 word2 少末位的前缀,再补插上它的末字符;dp[i-1][j-1] + 1 是替换——两边各退一位先对齐,再把这对不同的字符换掉。三条路覆盖了处理末位的全部方式,取最小者即最优,方向别记混:上是删、左是插、左上是替换。
复杂度多少,空间怎么优化
时间复杂度 O(m·n),m、n 是两个单词的长度:整张表每格填一次,每次只做常数次比较。空间 O(m·n) 存全表;由于每格只依赖上一行和本行左边,可用滚动数组只保留一行、再拿一个变量暂存左上角值,空间降到 O(n)。
编辑距离(也叫 Levenshtein 距离)是拼写纠错、DNA 序列比对、模糊搜索的底层度量,这张「前缀对前缀」的二维表也是最长公共子序列等一整族双串动态规划的通用骨架,值得吃透。
▶ 动画逐步走查(共 41 步)——想跟着动画一帧帧对照就展开
- 3记住这条转移,下面每个内部格都在套它。
- 4左上角:空串变空串,不用动,填 0。下面先把「免费基准」首行首列铺好。
- 5首行:word1 是空串,要变出 "r" 只能插入 1 次 → dp[0][1]=1。
- 6首行:word1 是空串,要变出 "ro" 只能插入 2 次 → dp[0][2]=2。
- 7首行:word1 是空串,要变出 "ros" 只能插入 3 次 → dp[0][3]=3。
- 8首列:word2 是空串,把 "h" 删光要删 1 次 → dp[1][0]=1。
- 9首列:word2 是空串,把 "ho" 删光要删 2 次 → dp[2][0]=2。
- 10首列:word2 是空串,把 "hor" 删光要删 3 次 → dp[3][0]=3。
- 11首列:word2 是空串,把 "hors" 删光要删 4 次 → dp[4][0]=4。
- 12首列:word2 是空串,把 "horse" 删光要删 5 次 → dp[5][0]=5。
- 13'h'≠'r',三条路各 +1:删上面=2、增左边=2、改左上=1,挑最小。
- 14取最小走「改」:dp[1][1]=1+0=1。
- 15'h'≠'o',三条路各 +1:删上面=3、增左边=2、改左上=2,挑最小。
- 16取最小走「改」:dp[1][2]=1+1=2。
- 17'h'≠'s',三条路各 +1:删上面=4、增左边=3、改左上=3,挑最小。
- 18取最小走「改」:dp[1][3]=1+2=3。
- 19'o'≠'r',三条路各 +1:删上面=2、增左边=3、改左上=2,挑最小。
- 20取最小走「改」:dp[2][1]=1+1=2。
- 21'o' 和 'o' 一样,这俩字母天然对上、不用动,直接抄左上角 1。
- 22落子:dp[2][2]=1(继承左上角)。
- 23'o'≠'s',三条路各 +1:删上面=4、增左边=2、改左上=3,挑最小。
- 24取最小走「增」:dp[2][3]=1+1=2。
- 25'r' 和 'r' 一样,这俩字母天然对上、不用动,直接抄左上角 2。
- 26落子:dp[3][1]=2(继承左上角)。
- 27'r'≠'o',三条路各 +1:删上面=2、增左边=3、改左上=3,挑最小。
- 28取最小走「删」:dp[3][2]=1+1=2。
- 29'r'≠'s',三条路各 +1:删上面=3、增左边=3、改左上=2,挑最小。
- 30取最小走「改」:dp[3][3]=1+1=2。
- 31's'≠'r',三条路各 +1:删上面=3、增左边=5、改左上=4,挑最小。
- 32取最小走「删」:dp[4][1]=1+2=3。
- 33's'≠'o',三条路各 +1:删上面=3、增左边=4、改左上=3,挑最小。
- 34取最小走「改」:dp[4][2]=1+2=3。
- 35's' 和 's' 一样,这俩字母天然对上、不用动,直接抄左上角 2。
- 36落子:dp[4][3]=2(继承左上角)。
- 37'e'≠'r',三条路各 +1:删上面=4、增左边=6、改左上=5,挑最小。
- 38取最小走「删」:dp[5][1]=1+3=4。
- 39'e'≠'o',三条路各 +1:删上面=4、增左边=5、改左上=4,挑最小。
- 40取最小走「改」:dp[5][2]=1+3=4。
- 41'e'≠'s',三条路各 +1:删上面=3、增左边=5、改左上=4,挑最小。
- 42取最小走「删」:dp[5][3]=1+2=3。
- 43右下角 dp[5][3]=3,就是把 "horse" 改成 "ros" 的最少步数。
⚠️ 容易写错的地方
✗ 错:首行首列忘初始化
✓ 对:dp[0][j]=j, dp[i][0]=i
空串到非空只能纯插/纯删
✗ 错:相同字母也 +1
✓ 对:相同时直接继承 dp[i-1][j-1]
已对上不需任何操作
✗ 错:三条来路记混
✓ 对:删=上、增=左、改=左上
方向错则取值错
完整代码(Python / C++ / Java)
Python
def minDistance(w1, w2):
m, n = len(w1), len(w2)
dp = [[0]*(n+1) for _ in range(m+1)]
for j in range(n+1): dp[0][j] = j
for i in range(m+1): dp[i][0] = i
for i in range(1, m+1):
for j in range(1, n+1):
if w1[i-1] == w2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[m][n]C++
int minDistance(string w1, string w2){
int m = w1.size(), n = w2.size();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(int j=0;j<=n;j++) dp[0][j]=j;
for(int i=0;i<=m;i++) dp[i][0]=i;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
if(w1[i-1]==w2[j-1]) dp[i][j]=dp[i-1][j-1];
else dp[i][j]=1+min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]});
return dp[m][n];
}Java
int minDistance(String w1, String w2){
int m = w1.length(), n = w2.length();
int[][] dp = new int[m+1][n+1];
for(int j=0;j<=n;j++) dp[0][j]=j;
for(int i=0;i<=m;i++) dp[i][0]=i;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
if(w1.charAt(i-1)==w2.charAt(j-1)) dp[i][j]=dp[i-1][j-1];
else dp[i][j]=1+Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1]));
return dp[m][n];
}复杂度
时间
O(m·n)
填满整张表
空间
O(m·n)
可滚动优化到 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 编辑距离 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么三条来路对应删/增/改?+
dp[i-1][j] 是少考虑 word1 一个字母→相当于删它;dp[i][j-1] 是少考虑 word2 一个字母→相当于插入它;dp[i-1][j-1] 是两边各退一格→相当于把这对替换。
怎么把空间降到 O(n)?+
dp[i][j] 只用到上一行和本行左边,用滚动数组保留一行 + 一个左上角暂存即可。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 编辑距离 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。