【算法】力扣72. 编辑距离
题目描述
给你两个单词 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')
思路解析
这是一个双串问题,我们可以从二维dp数组开始思考,将这两个维度的状态分别分给两个串。
状态定义:
dp[i][j]为长度为 i 的 word1 与长度为 j 的 word2 所需的最少操作数。
Base Case:
dp[0][0] = 0dp[0][j] = jj ∈ [1, m]dp[n][0] = ni ∈ [1, n]
n是word1的长度,m是word2的长度
状态转移:
若 word1[i] == word2[j],则不需要进行操作。
对于不相等的字符,需要考虑插入、删除和替换三种操作。
插入操作
-
插入到 word1 中:
insr1 = dp[i - 1][j] + 1 -
插入到 word2 中:
insr2 = dp[i][j - 1] + 1 -
解释: 对于插入,如果对
word1进行插入,在长度为i - 1的word1和长度为j的word2的状态下,即dp[i - 1][j],因为插入而转移到dp[i][j];对word2进行插入也是同理,即dp[i][j - 1]转移到dp[i][j]。
删除操作
- 删除 word1 的字符:
dele1 = dp[i - 1][j] + 1 - 删除 word2 的字符:
dele2 = dp[i][j - 1] + 1 - 解释: 与上文的插入操作类似,如果要删除word1的第
i个字符,那么就相当于不需要匹配word1的第i个字符,此时可以从长度为i - 1的word1的状态转移过来。
替换操作
-
替换操作:
repl = dp[i - 1][j - 1] + 1 -
解释: 既然
word1[i]和word2[j]被替换成一样的字符,那么就相当于在dp[i - 1][j - 1]这个状态中往word1和word2两边增加一个相同的字符使得word1的长度变为i,word2的长度变为j。
最终,dp[i][j] 的值为这些操作中的最小值。
PS:你可能注意到了有些状态转移方程是一样的,但是这里为了直观易理解,在朴素DP的代码中,不进一步抽象简化。
朴素DP
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n, m = len(word1), len(word2)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
dp[i][0] = i
for i in range(1, m + 1):
dp[0][i] = i
for i in range(1, n + 1):
for j in range(1, m + 1):
if word1[i - 1] != word2[j - 1]:
insr1 = dp[i - 1][j] + 1
insr2 = dp[i][j - 1] + 1
dele1 = dp[i - 1][j] + 1
dele2 = dp[i][j - 1] + 1
repl = dp[i - 1][j - 1] + 1
dp[i][j] = min(insr1, insr2, dele1, dele2, repl)
else:
dp[i][j] = dp[i - 1][j - 1]
return dp[n][m]
复杂度分析
- 时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m),其中 n 和 m 分别为 word1 和 word2 的长度。
- 空间复杂度: O ( n ∗ m ) O(n*m) O(n∗m),需要一个二维数组来存储状态信息。
滚动数组优化DP
对于每一个dp[i],我们实际上至多只会用两次,例如,当i = 1的时候,会用到dp[1][j - 1],即dp[i][j - 1],当i = 2的时候,会用到dp[1][j],即dp[i - 1][j]。
那么,我们可以用滚动数组的思路,把dp表优化成一个内嵌两个数组的二维数组。
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n, m = len(word1), len(word2)
dp = [[0] * (m + 1) for _ in range(2)]
for j in range(1, m + 1):
dp[0][j] = j
for i in range(1, n + 1):
dp[0][0] = i - 1
dp[1][0] = i
for j in range(1, m + 1):
if word1[i - 1] != word2[j - 1]:
insr1_dele1 = dp[0][j] + 1
insr2_dele2 = dp[1][j - 1] + 1
repl = dp[0][j - 1] + 1
dp[1][j] = min(insr2_dele2, insr1_dele1, repl)
else:
dp[1][j] = dp[0][j - 1]
# 更新滚动数组
dp[0] = dp[1]
dp[1] = [0] * (m + 1)
return dp[0][m]
步骤解析
- 初始化滚动数组: 首先,我们初始化一个长度为2的滚动数组
dp,其中dp[0]和dp[1]分别代表两个相邻的行。
dp = [[0] * (m + 1) for _ in range(2)]
- 初始化第一行: 对于第一行,我们按照基本的编辑距离规则进行初始化,即
dp[0][j] = j,表示从空字符串到word2的编辑距离。
for j in range(1, m + 1):
dp[0][j] = j
- 滚动数组核心逻辑: 在计算编辑距离的过程中,我们使用两行来交替存储当前行和上一行的状态信息。通过滚动数组,我们可以优化空间复杂度。
for i in range(1, n + 1):
# 在原本的朴素解法中,dp[i][0] = i 的base case
dp[0][0] = i - 1
dp[1][0] = i
for j in range(1, m + 1):
if word1[i - 1] != word2[j - 1]:
insr1_dele1 = dp[0][j] + 1
insr2_dele2 = dp[1][j - 1] + 1
repl = dp[0][j - 1] + 1
dp[1][j] = min(insr2_dele2, insr1_dele1, repl)
else:
dp[1][j] = dp[0][j - 1]
# 滚动数组
dp[0] = dp[1]
dp[1] = [0] * (m + 1)
- 返回结果: 最终,编辑距离的结果存储在
dp[0][m]中,即最后一行的最后一个元素。
复杂度分析
- 时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m),其中 n 和 m 分别为 word1 和 word2 的长度。
- 空间复杂度: O ( 2 ∗ m ) O(2*m) O(2∗m),仅仅保存上一层的结果,不需要保存整个dp表。
总结
通过动态规划的思想,我们成功地解决了编辑距离问题。通过分析状态转移方程,我们能够清晰地理解每一步的操作是如何影响最终结果的。在代码实现中,我们使用了二维数组 dp 来存储状态,通过填表的方式计算最终的编辑距离。同时,我们也介绍了使用滚动数组的优化方案,大大减少了空间复杂度,更高效地解决了问题。

705

被折叠的 条评论
为什么被折叠?



