前端算法必刷题系列[79]

298 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

147. 编辑距离 (edit-distance)

标签

  • 动态规划
  • 困难

题目

leetcode 传送门

给你两个单词 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. 寻找最优子结构(状态表示)
  2. 归纳状态转移方程(状态计算)
  3. 边界初始化

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种修改方式

    1. 把字符 word1[i] 替换成与 word2[j] 相等,替换个字符,操作需要一步
    • dp[i][j] = dp[i-1][j-1] + 1
    1. word1[i] 多了,删除就行, 加一步删除操作
    • dp[i][j] = dp[i-1][j] + 1
    1. word1[i] 少了,需要在 word1 末尾插入一个与 word2[j] 相等的字符
    • dp[i][j] = dp[i][j-1] + 1

而我们要最优的情况,也就是使得 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,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考