BM75 编辑距离(一)¶
较难通过率 44.53%python3核心代码模式样例通过牛客 AC字符串动态规划
讲解章节:线性 DP
一句话
插入/删除/替换三选一取最小,再把二维表滚动压成一行。
解题思路¶
这题考什么¶
双序列 DP 的标准形态:状态用两个前缀长度描述,转移按「两串的末尾字符怎么对齐」分类, 见 线性 DP。
状态定义:dp[i][j] = 把 str1 的前 i 个字符变成 str2 的前 j 个字符的最少操作数。 边界(一方为空时只能一直插或一直删):
转移方程,记 a = str1[i-1]、b = str2[j-1]:
a == b: dp[i][j] = dp[i-1][j-1] 末尾天然对齐,不花操作
a != b: dp[i][j] = 1 + min(dp[i-1][j-1], 把 a 替换成 b
dp[i-1][j], 删掉 a
dp[i][j-1]) 在 str1 末尾插入 b
三个候选恰好对应题面允许的三种操作,且都把问题缩小一格,无后效性成立。 答案是 dp[m][n]。
每一行只依赖上一行的同列与左上角,以及本行的左邻, 所以行这一维可以滚掉,只留长度 n+1 的一维数组: 扫到第 j 列时 dp[j] 尚未覆盖(正是「上」),dp[j-1] 已覆盖(正是「左」), 而「左上角」在 dp[j] 被覆盖的瞬间就丢了——所以要用临时变量 diag 在覆盖之前把它接住,每列末尾再把本列的「上」交棒给下一列的「左上」。 这是滚动数组里最典型的一处陷阱,见 DP 入门。
数据规模与复杂度¶
两串长度均 <= 1000,只含小写字母,时限 2 秒(其他语言)。 时间 O(m*n) 最坏 1e6 次内层迭代,判题机比本机慢数倍, 所以内层刻意压到最少操作:三路取最小用手写比较而不是 min(), 每轮先把 str1[i-1] 存进局部变量 a,避免每格重复寻址。 空间 O(min(m, n)):把较短的串放在内层维度上,滚动数组最长也只有 1001 项。
坑在哪¶
- diag 的维护顺序不能颠倒:进入内层前 diag = dp[0](旧的 dp[i-1][0]), 然后才把 dp[0] 改写成 i。先写 dp[0] = i 会让第一列的左上角变成本行的值,整行错。
- 每列结束时 diag = up(本列的「上」),因为下一列的「左上」正是本列的「上」。 漏掉这一步,diag 会一直停在行首,后面所有替换操作都基于错误的对角值。
- 交换两串是安全的:插入与删除互为逆操作、替换与自身互逆, 所以编辑距离是对称的,d(str1, str2) == d(str2, str1)。 交换只为让滚动数组取到 min(m, n) 的长度,不影响答案。
- n == 0 时答案就是 m(把 str1 删光),要在建表前挡掉—— 否则 dp 只有 dp[0] 一项,内层循环不进,返回的是 dp[0] 而不是 m。
- 字符相同时是直接继承 dp[i-1][j-1],不是 dp[i-1][j-1] + 0 之外还要与另外两项取最小。 写成一律取三者最小再加判断,会在等值时凭空多算操作。
样例复核¶
"nowcoder" 与 "new":交换后内层维度是长度 3 的 "new", 最终 dp[3] = 6,对应「替换 o 为 e、删掉 coder 的 5 个字符」,与期望输出一致。
参考实现¶
[:octicons-arrow-left-16: BM74](BM74.md) [BM76 :octicons-arrow-right-16:](BM76.md)