题目链接
https://leetcode.com/problems/edit-distance/
题目描述
给定两个单词word1和word2,计算由word1转化为word2需要的最少操作数。
可以对一个单词进行以下三种操作:(1)插入一个字符;(2)删除一个字符;(3)替换一个字符。
示例
输入:word1=“horse”,word2="ros"
输出:3
horse->rorse(将'h'替换为'r')
rorse->rose(删除'r')
rose->ros(删除'e')
解题思路
利用自底向上的动态规划算法来求解。
令dp[i][j]表示word1的前i个元素(word1[0],...,word1[i-1])转化为word2的前j个元素(word2[0],...,word2[j-1])需要的最少操作数。base case为i=0或者j=0时的情况,当i=0时,dp[0][j]表示一个空字符串转化为word2的前j个元素所需要的最少操作数,因此有dp[0][j] = j,也就是要进行j次插入字符操作;当j=0时,dp[i][0]表示word1的前i个元素转化为空字符串所需要的最少操作数,因此有dp[i][0] = i,即要进行i次删除字符操作。
接下来进行将dp[i][j]的取值问题分解为子问题:
如果word1[i-1] == word2[j-1],word1的第i个元素和word2的第j个元素相等(由于需要操作数最少,那么这里就不应该做操作),此时dp[i][j]就等于word1前i-1个元素转化为word2前j-1个元素所需要的最少操作数,即dp[i-1][j-1]。
如果word1[i-1] != word2[j-1],由于在更新dp[i][j]之前dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1]都已经更新完了,即对应的最少操作数都计算出来了,当前可选的选择为:
(1)进行字符替换,将word1[i-1]换为word2[j-1],此时dp[i][j] = dp[i-1][j-1] + 1,即word1前i-1个元素(word1[0],...,word1[i-2])转化为word2前j-1个元素(word2[0],...,word2[j-2])所需要的最少操作数+当前替换操作。
(2)进行字符删除,将word1[i-1]删除,此时dp[i][j] = dp[i-1][j] + 1,即word1前i-1个元素(word1[0],...,word1[i-2])转化为word2前j个元素所需要的最少操作数+当前删除操作。
(3)进行字符插入,在word1[i-1]后面插入一个和word2[j]相等的元素,此时dp[i][j] = dp[i][j-1]+1,即word1前i-1个元素转化为word2前j-2个元素所需要的最少操作数+当前插入操作。
由于题目中要求的是最少操作数,因此要取dp[i][j]可能取值中的最小值来更新dp[i][j]。
对于示例中的word1=“horse”,word2="ros",更新结束后dp二维数组为:
再举个例子:
令word1="abc",word2="defgh",当前已知从"ab"到"def"之间的最小编辑距离,接下来计算从"abc"到"defg"之间的最小编辑距离,i = 3,j = 4:
由于'c'!= 'g',那么有三种选择:
(1)字符替换,将'c'替换成'g',dp[i][j] = dp[i-1][j-1] + 1。
(2)字符删除,如果已知"ab"和“defg”之间的最小编辑距离,可以将‘c’删除。dp[i][j] = dp[i-1][j] + 1。
(3)字符插入,如果已知“abc”和"def"之间的最小编辑距离,可以在'c'后面插入'g'。此时dp[i][j] = dp[i][j-1]+1。
Python实现
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m,n = len(word1),len(word2)
dp = [[0 for _ in range(n+1)] for _ in range(m+1)] #这里要注意,行数写外面,列数写里面
for j in range(0,n+1):
dp[0][j] = j
for i in range(0,m+1):
dp[i][0] = i
for i in range(1,m+1):
for j in range(1,n+1):
if(word1[i-1] == word2[j-1]):#这里也要注意,知道待比较的是什么
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1
return dp[m][n]
二维dp数组一般尺寸为(m+1) * (n+1),因为可以让dp[i][0]、dp[0][j]表示空字符串的情况。
时间复杂度与空间复杂度
时间复杂度为O(mn),空间复杂度为O(mn)。
状态压缩
由于dp[i][j]的更新仅与dp[i-1][j-1](左上角)、dp[i-1][j](左边)、dp[i][j-1](上边)有关,因此空间复杂度可以进一步减小,将二维数组投射到一维数组上。压缩时一般是将第一个维度i去掉,只保留j。(在行数大于列数的情况下,如果行数小于列数,则需要调用函数并改变参数位置。由word1转化为word2需要的最少操作数等于由word2转化为word1所需要的最少操作数)这个一维数组只能表示二维数组中的某一行dp[i][...],遍历i时,一维数组也随之刷新(外层循环i,内层循环j)。
因此当前需要关注的是dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]这些变量如何存储。在更新当前行时,未更新的dp[j]可以表示dp[i-1][j],更新后的dp[j-1]可以表示dp[i][j-1]。dp[i-1][j-1]为还没更新的dp[j-1],因此在内层循环更新dp[j]之前,先将dp[j]赋给tmp,更新完dp[j]后再将tmp赋值给prerow_precol,即当内层迭代到j+1时,prerow_precol存储的就是上次迭代中更新前的dp[j]了。
Python实现
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m,n = len(word1),len(word2)
if m < n: #认为word1长度大于word2
return self.minDistance(word2,word1)
dp = [i for i in range(n+1)] #base case
for i in range(1,m+1):
prerow_precol = dp[0]
dp[0] = i
for j in range(1,n+1):
tmp = dp[j]#没更新前的dp[j]对应二维数组中的dp[i-1][j]
if(word1[i-1] == word2[j-1]):
dp[j] = prerow_precol
else:
dp[j] = min(dp[j-1],prerow_precol,dp[j]) + 1
#更新prerow_precol
prerow_precol = tmp #将dp[i-1][j]保存到下一次循环中,就对应dp[i][j+1]的左上角元素
return dp[-1]
时间复杂度与空间复杂度
时间复杂度为O(mn),空间复杂度为O(min(m,n))