一篇文章解释清楚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] + 0
                    arr[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;
                }
                //更新p
                p = r;
            }
        }
        return dp[targetLen];
    }

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

posted @   道祖且长  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示