dp 数组含义定义二维数组 dp:
这里的操作包括三种:
注意 i 和 j 表示的是“前几个字符”,不是字符串下标。
例如:
题目要求把整个 word1 转换成整个 word2,所以最终答案就是:
假设当前正在计算 dp[i][j],也就是:
此时需要比较两个前缀的最后一个字符:
如果 word1[i - 1] === word2[j - 1],说明最后一个字符已经一样,不需要额外操作。
此时只需要看前面的字符怎么转换:
例如 "ho" 转换成 "ro" 时,最后一个字符都是 o,所以问题变成 "h" 转换成 "r"。
如果 word1[i - 1] !== word2[j - 1],最后一个字符不同,有三种选择。
第一种,删除 word1[i - 1]:
含义是:先把 word1 的前 i - 1 个字符转换成 word2 的前 j 个字符,再删除当前多出来的 word1[i - 1]。
第二种,插入 word2[j - 1]:
含义是:先把 word1 的前 i 个字符转换成 word2 的前 j - 1 个字符,再插入当前缺少的 word2[j - 1]。
第三种,替换 word1[i - 1]:
含义是:先把 word1 的前 i - 1 个字符转换成 word2 的前 j - 1 个字符,再把当前字符替换成目标字符。
三种操作中选择最小值:
所以完整转移方程是:
dp 数组如何初始化因为 dp[i][j] 表示前缀之间的转换,所以必须考虑空字符串。
dp[i][0] 表示把 word1 的前 i 个字符转换成空字符串。
只能不断删除,所以:
例如:
dp[0][j] 表示把空字符串转换成 word2 的前 j 个字符。
只能不断插入,所以:
例如:
如果 word1 或 word2 本身就是空字符串,也可以直接通过第一行或第一列得到答案。
根据状态转移方程:
也就是当前格子依赖:
所以按行从上到下、每一行从左到右遍历即可。
这样计算 dp[i][j] 时,它依赖的三个状态都已经计算完成。
dp 数组以 word1 = "horse"、word2 = "ros" 为例。
行表示 word1 的前缀,列表示 word2 的前缀:
word1 \ word2 | "" | r | o | s |
|---|---|---|---|---|
"" | 0 | 1 | 2 | 3 |
h | 1 | |||
o | 2 | |||
r | 3 | |||
s | 4 | |||
e | 5 |
"h" 转换成word2 的前缀dp[1][1] 表示 "h" 转换成 "r"。
h !== r,有三种选择:
取最小值,所以 dp[1][1] = 1。
继续计算这一行:
word1 \ word2 | "" | r | o | s |
|---|---|---|---|---|
"" | 0 | 1 | 2 | 3 |
h | 1 | 1 | 2 | 3 |
o | 2 | |||
r | 3 | |||
s | 4 | |||
e | 5 |
"ho" 转换成word2 的前缀dp[2][1] 表示 "ho" 转换成 "r"。
o !== r:
所以 dp[2][1] = 2。
dp[2][2] 表示 "ho" 转换成 "ro"。
最后一个字符都是 o,不需要操作:
dp[2][3] 表示 "ho" 转换成 "ros"。
o !== s:
所以 dp[2][3] = 2。
word1 \ word2 | "" | r | o | s |
|---|---|---|---|---|
"" | 0 | 1 | 2 | 3 |
h | 1 | 1 | 2 | 3 |
o | 2 | 2 | 1 | 2 |
r | 3 | |||
s | 4 | |||
e | 5 |
按照同样的规则继续计算:
word1 \ word2 | "" | r | o | s |
|---|---|---|---|---|
"" | 0 | 1 | 2 | 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,表示 "horse" 转换成 "ros" 最少需要 3 次操作。
一种对应的转换过程是:
fill 直接创建二维数组二维数组不能这样初始化:
原因是:new Array(n + 1).fill(0) 只创建了一次,外层 fill 会把同一个数组引用填充到每一行。
也就是说,表面上看起来 dp 有很多行:
但这些行其实都指向同一个内部数组。
所以当你修改其中一行时:
其他行的第 0 列也会一起变化,因为它们本质上是同一个数组。
可以用下面的例子验证:
这会直接破坏动态规划表,因为 dp[i][j] 本来应该表示一个独立状态,每一行必须互不影响。
正确写法是:
这里的回调函数 () => Array(n + 1).fill(0) 会为每一行都重新创建一个新的数组,所以每一行都是独立的。
可以理解为:
O(m * n),其中 m 是 word1 的长度,n 是 word2 的长度。O(m * n),需要二维数组保存所有状态。