一篇文章解释清楚leetcode经典动态规划问题:72.编辑距离

编程入门 行业动态 更新时间:2024-10-06 04:07:55

<a href=https://www.elefans.com/category/jswz/34/1752179.html style=一篇文章解释清楚leetcode经典动态规划问题:72.编辑距离"/>

一篇文章解释清楚leetcode经典动态规划问题:72.编辑距离

吐槽

题意简单,但是它就是难,让人摸不着头脑,手足无措的难。

思考一下

由于是求最值,第一反应就是往动态规划方向想,但是就算是套用动态规划的思路框架:

1、思考如何使用数组来表达状态,即dp table
2、思考如何写出状态转移方程
3、思考如何定义初始数据,即base case
4、思考如何优化执行效率

大部分人就会发现,连第一步都想不出来;增、删、替换这些个操作,如何体现为数组的操作。
更不用说写出状态转移方程了。

既然直接写出动态规划的写法太难了,那我们就换个思路,先根据下面的思路,层层推导试试:
递归的暴力解法 => 带备忘录的递归解法 => 非递归的动态规划解法

递归解法

就算是递归,也必须先要知道思路才行,也就是如何表现增、删、替换这些操作,并且记录操作数。这也是这题最难最难的地方。

例如,我们有两个字符串,分别为word1:“pro”, word2:“apple”

我们通过两个字符串分别从后往前一步步比较,一步步删减,然后已经处理的字符就不再管他。

如果是删除:


我们看图片可以发现,如果是删除,那么word1的长度就会变短,word2不会变,并且操作总数增加了1

由于算法的函数签名如下:

public int minDistance(String word1, String word2)

模模糊糊之间,我们似乎可以建立一个未必正确的,针对删除操作的递归方法:

minDistance(word1.substring(0,word1.length() - 1), word2) + 1;
如果是插入:

我们看图片可以发现,如果是插入,那么word1剩余需要处理的字符长度没变,还是“pro”,但是word2需要对比处理的字符长度变少了,少了个"e",后面只需要处理"appl"就行,并且操作总数增加了1

模模糊糊之间,我们似乎可以建立一个未必正确的,针对插入操作的递归方法:

minDistance(word1, word2.substring(0,word2.length() - 1)) + 1;
如果是替换:


我们看图片可以发现,如果是替换,那么word1剩余需要处理的字符长度减一,变成“pr”,并且word2需要对比处理的字符长度也变少了,少了个"e",后面只需要处理"appl"就行,并且操作总数增加了1

模模糊糊之间,我们似乎可以建立一个未必正确的,针对替换操作的递归方法:

minDistance(word1.substring(0,word1.length - 1), word2.substring(0,word2.length() - 1)) + 1;

但是转念一想,不对啊,如果word1当前处理的字符,正好与word2当前处理的字符一样呢?那岂不是不用替换了?那操作总数岂不是不用加一了?

这种思考是正确的,如果两个字符相同,替换可以替换,保证递归进行下去,但是操作数不需要加一,所以需要判断一下两个字符是否相等,相等的话操作数就不用加一了。

  int d;if (word1.charAt(word1.length() - 1) == word2.charAt(word2.length() - 1)) {d = 0;} else {d = 1;}minDistance(word1.substring(0,word1.length() - 1), word2.substring(0,word2.length() - 1)) + d;

也许有些人会有些疑惑,如果两个字符相同,为啥删除和插入的操作数就无脑加一?
替换就要判断一下?

这其实非常好理解,正如我上文所说:如果两个字符相同,替换还是会进行替换操作,保证递归进行下去,但是操作数不需要加一,需要跳出代码,从实际想一想。在这里,递归方法的本质是穷举。

ok,前文中的疑问:增、删、替换三种操作如何用代码体现?就得到了解答

递归就两个关键点,其一是递归条件,其二是退出条件

退出条件

看到这一步了退出条件也就非常非常容易推导出来了:

要么word1处理完了,word2没处理完,返回word2没处理完的字符串长度就行(插入操作)
要么word2处理完了,word1没处理完,返回word1没处理完的字符串长度就行(删除操作)
要么word1处理完了,word2也正好处理完,返回0(不需要再操作了操作)

代码:

if (word1.length() == 0) {return word2.length();} else if (word2.length() == 0) {return word1.length();} else if (word1.equals(word2)) {return 0;}

那么递归的两个要素就出来了,完整代码也就出来了。只需要在每次递归中,在增、删、替换三种操作方法中,操作数最小的作为选择就行了。

   public int minDistance(String word1, String word2) {//退出条件if (word1.length() == 0) {return word2.length();} else if (word2.length() == 0) {return word1.length();} else if (word1.equals(word2)) {return 0;}int d;if (word1.charAt(word1.length() - 1) == word2.charAt(word2.length() - 1)) {d = 0;} else {d = 1;}//插入int a = minDistance(word1, word2.substring(0,word2.length() - 1)) + 1;//删除int b = minDistance(word1.substring(0,word1.length() - 1), word2) + 1;//替换,如果两个字符相同,都为字符a,则可以思考为a替换为a,做了操作,但是没必要,为了取得最小编辑次数,不加操作数int c = minDistance(word1.substring(0,word1.length() - 1), word2.substring(0,word2.length() - 1)) + d;return Math.min(Math.min(a,b),c);}

递归总结

递归还是记住最基本的两点,一是递归条件,二是退出条件,牢记这两个基本点,题目多做一点,很多题看到题目就会有思路的。
这一题写是写出来了,但是效率非常的低,leetcode中也会超时。所以接下来要考虑动态规划了

动态规划解法

在上面的递归解法中,我们知道了基本思路,包括如何体现增、删、改的操作。而在动态规划中,基本都是使用数组来代替状态的变化,最关键的是如何定义这个数组,使得状态转移更加便利。

我们都知道动态规划可以借助dp table来推到最优值,如何是本题的dp table
先初始化表格(左边为增,上边为删。如果全部是增,或者全部是删,都是多少个字符就多少步,所以如此初始化)

接下来就开始推导了,根据递归方法中提到的思路:如果两个字符相等,则增、删操作数加一,替换操作数不加;如果两个字符不相等,则增、删、替换操作数都加一。以左上角为替换操作,左边和上边为增和删操作,进行推导如下:

最右下角就是最终的结果

举个简单的例子帮助理解推导过程:

推导过程出来了,代码也就可以参照递归方法写出来了:

public int minDistance(String word1, String word2) {int sourceLen = word1.length();int targetLen = word2.length();if(sourceLen == 0){return targetLen;}if(targetLen == 0){return sourceLen;}//定义矩阵(二维数组)int[][]  arr = new int[sourceLen+1][targetLen+1];//赋予初值for(int i=0; i < sourceLen+1; i++){arr[i][0] = i;}for(int j=0; j < targetLen+1; j++){arr[0][j] = j;}Character sourceChar = null;Character targetChar = null;for(int i=1; i < sourceLen+1 ; i++){sourceChar = word1.charAt(i-1);for(int j=1; j < targetLen+1 ; j++){targetChar = word2.charAt(j-1);if(sourceChar.equals(targetChar)){// 如果source[i] 等于target[j],则替换操作就是最小值:d[i, j] = d[i-1, j-1] + 0arr[i][j] = arr[i-1][j-1];}else{//source[i] 不等于target[j],则需要取三个操作中的最小值arr[i][j] = (Math.min(Math.min(arr[i-1][j], arr[i][j-1]), arr[i-1][j-1])) + 1;}}}return arr[sourceLen][targetLen];}

如果文章认真从头看到尾,代码就非常非常好理解了,思路也清晰了,动态规划的题确实只能总结框架,不能每题都照搬照套,都需要自己思考才能够做出来,这也是为啥大厂面试题都喜欢考动态规划的题目了。

以上的算法确实是动态规划,也可以通过leetcode的提交,但是效率不高,我此刻提交的结果是耗时击败20%,内存击败70%。

因为有一个细节,既然每个dp[i][j]只和它附近的三个状态有关,空间复杂度是可以压缩成 O(min(M,N)) 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,并且优化后提升也并不算不大,优化后的代码也放出来吧:

    public int minDistance(String word1, String word2) {int sourceLen = word1.length();int targetLen = word2.length();if(sourceLen == 0){return targetLen;}if(targetLen == 0){return sourceLen;}//定义矩阵(二维数组)int[]  dp = new int[targetLen + 1];//赋予初值for(int j=0; j < targetLen+1; j++){dp[j] = j;}for(int i=1; i < sourceLen+1 ; i++){int p = dp[0];dp[0] = i;for(int j=1; j < targetLen+1 ; j++){int q = dp[j - 1];    // dp[i][j - 1]int r = dp[j];   // dp[i - 1][j]if(word1.charAt(i-1) == word2.charAt(j-1)){dp[j] = p;}else{dp[j] = Math.min(Math.min(p, q), r) + 1;}//更新pp = r;}}return dp[targetLen];}

以上就是全部内容了,花费我大半天时间才肝出来,太难了,不过写这篇文章也确实让我对这题的理解,以及动态规划的理解更加透彻了,时间花的不亏。

更多推荐

一篇文章解释清楚leetcode经典动态规划问题:72.编辑距离

本文发布于:2024-02-13 13:14:08,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1758546.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:一篇文章   距离   编辑   经典   动态

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!