编辑距离(题号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')
提示:
- 0 <= word1.length, word2.length <= 500
- word1 和 word2 由小写英文字母组成
链接
解释
不愧是困难题,看的笔者一头雾水。
即使知道是动态规划,可还是想不到有什么规律,主要就是被添加、删除、修改吓到了,其实题目本没有这么难。
首先,正常思考下这个问题,从word1
变到word2
应该怎么操作。
肯定是需要一步步的变化的,那么应该怎么一步步变化呢?
举个🌰:
word1
-> funword2
-> find
首先一步步进行比较,将单词拆开,用i
来表示word1
的循环,用j
来表示word2
的循环。
那么可以得出一个状态:dp[i][j]
就是当word1
前i
个字符等于word2
的前j
个字符的最小操作数。
比方说dp[1][2]
,就是当word1
为f
时,word2
为fi
时,这之间的最小操作数,那么的i
的最大值和j
的最大值,就是最后我们需要的答案。
那么再看dp[i][j]
要怎么一步步求出来,也就拿到DP方程,有两种情况👇:
-
word1[i-1] === word2[j-1]
遇到这种情况就偷着乐吧,意思是在上一步时候,两个单词的字母是一样的,那么到了下一步就什么都不用做,直接去
dp[i-1][j-1]
即可 -
word1[i-1] !== word2[j-1]
如果不相等,那么就需要进行增、删、改的操作了,这三种操作对应的状态分别是:
dp[i-1][j]
:word1
删掉一个字符串dp[i][j-1]
:word1
添加一个字符串dp[i-1][j-1]
:word1
修改一个字符串
那么此时整体的DP方程就是:
if (word1[i-1] === word2[j-1]) {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
}
不相等时因为需要操作,所以最小步数后面要加1,相等就不需要操作了。
到了这里DP方程就出来了,剩下的难点就是处理初始值的情况了,说实话,初始值这块笔者理解的时间比DP方程还要多,总是GET不到点。
后来发现原来初始值的赋值就是为了解决word1
或者word2
为空字符串时的情况,这么一想就茅塞顿开了。
如果word1
为空字符串,那么只能依次添加word2
里面的字母,那么此时的dp
数组是👇:
dp[0] = [0, 1, ,2, 3 ... j]
如果word2
为空字符串,那么只能在word1
中依次删除字母,此时的dp
数组是👇:
dp[0][0] = 0
dp[1][0] = 1
dp[2][0] = 2
...
dp[i][0] = i
因为两个字符串都可能为空,所以需要循环的数组需要比word1
和word2
的长度多一位,那多在哪里呢?
多在第一位,所以dp[0][0]
是空字符串,也就是word1
和word2
都是0的情况。
所以说在👆的🌰种,dp
数组的初始值应该是:
[
[0, 1, 2, 3, 4],
[1],
[2],
[3],
]
下面再进行两次循环就可以得出正确的答案了。
思路确实比较复杂,没了解过这类题目的一下来应该很难能想到答案,不过现在就是遇到过啦~下次就应该记得处理了。
👇看代码
自己的答案
无
更好的方法(动态规划:二维数组)
var minDistance = function(word1, word2) {
var len1 = word1.length
len2 = word2.length
dp = Array.from({length: len1 + 1}, () => ([]))
for (let i = 0; i < len1 + 1; i++) {
dp[i][0] = i
}
for (let j = 0; j < len2 + 1; j++) {
dp[0][j] = j
}
for (let i = 1; i < len1 + 1; i++) {
for (let j = 1; j < len2 + 1; j++) {
if (word1[i-1] === word2[j-1]) {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
}
}
}
return dp[len1][len2]
}
对吧,经典二维数组,没啥可说的,和解释中的思路一摸一样,照着思路写就行。
更好的方法(动态规划:降维)
那既然说到了动态规划,而且是二维数组的动态规划,那就少不了降维操作了,这里其实也是可以将二维数组降维成一维数组的。
因为仔细看👆的方法,就能发现每次循环其实只涉及到上一次循环的结果,也就是dp[i-1]
的结果,那是不是可以把dp[i-1]
作为一个一维数组用来迭代呢?
显然是可以的,但有两点点需要注意,首先看这里👇:
if (word1[i-1] === word2[j-1]) {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
}
这一步需要注意的地方就是dp[i-1][j-1]
了,因为这一步取的是上一步的值,也就是这个值不用加1,这个值需要被提取出来,否则该值会被覆盖掉。
另外一个地方就是这里👇:
for (let i = 0; i < len1 + 1; i++) {
dp[i][0] = i
}
这里是给每个子数组的第一位进行赋值操作,为了解决word2
为空字符串的情况,所以在第二层j
循环开始前,需要把dp[0]
修改为当前的i
。
那么最后的代码就是👇:
var minDistance = function(word1, word2) {
var len1 = word1.length
len2 = word2.length
dp = new Array(len2)
for (let j = 0; j < len2 + 1; j++) {
dp[j] = j
}
for (let i = 1; i < len1 + 1; i++) {
var temp = dp[0]
dp[0] = i
for (let j = 1; j < len2 + 1; j++) {
var pre = temp
temp = dp[j]
if (word1[i-1] === word2[j-1]) {
dp[j] = pre
} else {
dp[j] = Math.min(dp[j], dp[j-1], pre) + 1
}
}
}
return dp[len2]
};
得益于ES6的语法改进,👆的:
var pre = temp
temp = dp[j]
可以使用解构赋值操作进行替换:
[pre, temp] = [temp, dp[j]]
不过这样的话可读性略差,为了以防看不懂,笔者这里还是选择了维持原样。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇