这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。
147. 编辑距离 (edit-distance)
标签
- 动态规划
- 困难
题目
给你两个单词 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')
基本思路
遇到这种最优问题,还是想到用动规方法解决。我们之前的一般步骤是下面几步:
- 寻找最优子结构(状态表示)
- 归纳状态转移方程(状态计算)
- 边界初始化
1. 状态表示
首先是状态表示,关注点就是题目的目标
,从目标来找需要我们表示的变量
- 目标是将
word1
转换成word2
所使用的最少操作数
那我们就定义 dp[i][j]
的含义为:
- 当字符串 word1 的长度为
i
,字符串 word2 的长度为j
时,将 word1 转化为 word2 所使用的最少操作次数为dp[i][j]
。
2. 状态转移
这步有的时候非常难想,需要多看,多想
这里有个技巧就是一般
dp[i][j]
转移都是跟dp[i-1][j-1]
/dp[i-1][j]
/dp[i][j-1]
/存在转移关系。然后各种取最大最小值
。
再回顾下目标,我们要把 word1
转换成 word2
求所使用的最少操作数
思考如果 word1[i] 跟 word2[j] 相同,是不是不用改,那么
dp[i][j] = dp[i-1][j-1]
我们需要思考,有下面3种修改方式
-
- 把字符 word1[i] 替换成与 word2[j] 相等,
替换
个字符,操作需要一步
dp[i][j] = dp[i-1][j-1] + 1
- 把字符 word1[i] 替换成与 word2[j] 相等,
-
- word1[i] 多了,删除就行, 加一步
删除
操作
dp[i][j] = dp[i-1][j] + 1
- word1[i] 多了,删除就行, 加一步
-
- word1[i] 少了,需要在 word1 末尾
插入
一个与 word2[j] 相等的字符
dp[i][j] = dp[i][j-1] + 1
- word1[i] 少了,需要在 word1 末尾
而我们要最优的情况,也就是使得 dp[i][j] 的值最小,可以推导出
dp[i][j] = Math.min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
3. 边界初始化
一般边界都是从 0 开始
的
我们就思考,i === 0
或者 j === 0
的含义, 只要一个是 0, 那转换过程其实就非常简单,要么加要么删, 则
dp[0][j] = dp[0][j-1] + 1 // 不停的加
dp[i][0] = dp[i-1][0] + 1 // 不停的删
好了经过这三步,我们的代码就呼之欲出了, 下面是实现
写法实现
var minDistance = function(word1, word2) {
const n1 = word1.length, n2 = word2.length
let dp = new Array(n1+1).fill(0).map(it => new Array(n2+1).fill(0))
// console.log(dp)
// [
// [ 0, 0, 0, 0 ],
// [ 0, 0, 0, 0 ],
// [ 0, 0, 0, 0 ],
// [ 0, 0, 0, 0 ],
// [ 0, 0, 0, 0 ],
// [ 0, 0, 0, 0 ]
// ]
// 初始化,当有一个字符串长度走到0时,另一个想要一样只能是插入或删除,也就是操作 + 1
for (let i = 1; i <= n1; i++) {
dp[i][0] = dp[i-1][0] + 1
}
for (let j = 1; j <= n2; j++) {
dp[0][j] = dp[0][j-1] + 1
}
// console.log(dp)
// [
// [ 0, 1, 2, 3 ],
// [ 1, 0, 0, 0 ],
// [ 2, 0, 0, 0 ],
// [ 3, 0, 0, 0 ],
// [ 4, 0, 0, 0 ],
// [ 5, 0, 0, 0 ]
// ]
for (let i = 1; i <= n1; i++) {
for (let j = 1; j <= n2; 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-1], dp[i-1][j], dp[i][j-1]) + 1
}
}
}
// console.log(dp)
// [
// [ 0, 1, 2, 3 ],
// [ 1, 1, 2, 3 ],
// [ 2, 2, 1, 2 ],
// [ 3, 2, 2, 2 ],
// [ 4, 3, 3, 2 ],
// [ 5, 4, 4, 3 ]
// ]
// 右下角就是所求的结果,最小编辑距离
return dp[n1][n2]
};
let word1 = "horse", word2 = "ros" // 3
console.log(minDistance(word1, word2))
优化算法
我们思考,其实我们是一行行推导,也就是计算第 i
行的数据,其实只是跟第 i-1
行数据有关
简单做个图
// dp 是个二维数组
[ (i-1, j-1) (i-1, j) (i, j-1) (i, j)]
其实,每次计算 dp[i][j] 只跟这几个数相关,我们计算完下一行就可以抹去上一行,原地修改,降维成一维数组
, 看代码
var minDistance = function(word1, word2) {
let n1 = word1.length, n2 = word2.length
let dp = new Array(n2+1).fill(0)
for (let j = 1; j <= n2; j++) {
dp[j] = j
}
// console.log(dp)
// dp: [ 0, 1, 2, 3 ]
// 对比上一解法,其实就是第一行(i=0)时的dp
// 接下来就是根据上一行推导下一行数据
for (let i = 1; i <= n1; i++) {
// dp[0] 其实就是 dp[i-1][j-1] 存一个临时变量
let temp = dp[0]
dp[0] = i
for (let j = 1; j <= n2; j++) {
let pre = temp
temp = dp[j]
if (word1[i-1] === word2[j-1]) {
dp[j] = pre
} else {
dp[j] = Math.min(dp[j-1], dp[j], pre) + 1
}
}
}
// console.log(dp)
// dp: [ 5, 4, 4, 3 ]
// 对比上一解法,其实就是最后一行(i=n1)时的dp
return dp[n2]
};
let word1 = "horse", word2 = "ros" // 3
console.log(minDistance(word1, word2))
另外向大家着重推荐下这个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列
今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友
Or 搜索我的微信号infinity_9368
,可以聊天说地
加我暗号 "天王盖地虎" 下一句的英文
,验证消息请发给我
presious tower shock the rever monster
,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧