前端刷题路-Day28:编辑距离(题号72)

246 阅读3分钟

编辑距离(题号72)

题目

给你两个单词word1word2,请你计算出将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 由小写英文字母组成

链接

leetcode-cn.com/problems/ed…

解释

不愧是困难题,看的笔者一头雾水。

即使知道是动态规划,可还是想不到有什么规律,主要就是被添加、删除、修改吓到了,其实题目本没有这么难。

首先,正常思考下这个问题,从word1变到word2应该怎么操作。

肯定是需要一步步的变化的,那么应该怎么一步步变化呢?

举个🌰:

  • word1 -> fun
  • word2 -> find

首先一步步进行比较,将单词拆开,用i来表示word1的循环,用j来表示word2的循环。

那么可以得出一个状态:dp[i][j]就是当word1i个字符等于word2的前j个字符的最小操作数。

比方说dp[1][2],就是当word1f时,word2fi时,这之间的最小操作数,那么的i的最大值和j的最大值,就是最后我们需要的答案。

那么再看dp[i][j]要怎么一步步求出来,也就拿到DP方程,有两种情况👇:

  1. word1[i-1] === word2[j-1]

    遇到这种情况就偷着乐吧,意思是在上一步时候,两个单词的字母是一样的,那么到了下一步就什么都不用做,直接去dp[i-1][j-1]即可

  2. 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

因为两个字符串都可能为空,所以需要循环的数组需要比word1word2的长度多一位,那多在哪里呢?

多在第一位,所以dp[0][0]是空字符串,也就是word1word2都是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:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ