编辑距离|最小编辑代价|LeetCode72

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

 

示例 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')

 

分析

对于

输入:word1 = "horse", word2 = "ros"
输出:3
这个例子来说:

如果使用暴力算法,复杂度会上升到指数级别,这是不允许的。

对于这类题目,最好的方法应该是动态规划。

对于单词wordA,wordB的字母的add,delete,replace操作,可以这样分析:

删除单词A的一个字符=增加单词B的一个字符

替换单词A的一个字符=替换单词B的一个字符

反之同理。

所以,实际操作只有三种:

  1. 给单词A插入字符
  2. 给单词B插入字符
  3. 替换单词A或B的字符

这样就把情况减少到了三种,便于分析。

  • 给单词A插入字符:如果horse到ro的距离为a,那么horse到ros的距离显然不会超过a+1,因为最多只需要再增加一个字符s就可以达到目的,长度是a+1
  • 给单词B 插入字符:如果我们知道 hors 到 ros 的编辑距离为 b,那么显然 horse 到 ros 的编辑距离不会超过 b + 1,原因同上;
  • 修改单词 A 的一个字符:如果我们知道 hors 到 ro 的编辑距离为 c,那么显然 horse 到 ros 的编辑距离不会超过 c + 1,原因同上。

从 horse 变成 ros 的编辑距离应该为 min(a + 1, b + 1, c + 1)

那么我们继续分析a,b,c究竟代表什么。

设定一个dp[wordA.length+1][word2.length+1]来存放动态规划的信息。

dp[i][j]表示的是wordA[0...i],wordB[0...j]的编辑距离,这样我们就有了初始的数组信息。

        for (int i = 0; i <= length2; i++) {
            dp[0][i] = i;  //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
        }
        for (int i = 0; i <= length1; i++) {
            dp[i][0] = i;  // wordB长度为0的时候,wordA想要到达wordB,需要delete i个字母
        }

那么距离a代表什么呢?a代表的是给A插入字符。

给wordA插入字符才能达到dp[i][j],所以a对应的应该是dp[i-1][j],从wordA[0..i-1]这个长度->wordA[0...i]这个长度,dp[i][j]=dp[i-1][j];

所以,距离b代表的是给wordB插入一个字符,那么dp[i][j]=dp[i][j-1]+1;

距离c代表的是给wordA或者B替换一个字符,因为A或者B替换是等价的,现在理解成给A替换就好。在dp[i-1][j-1]时候,如果wordA[i]=wordB[i]那么就不需要替换,dp[i][j]=dp[i-1][j-1];但是wordA[i]!=wordB[j]时候,那么就需要将A的第i个字符替换为B的第j个字符wordA[i]<-wordB[j],dp[i][j]=dp[i-1][j-1]+1。

题目要求的是最小编辑距离,那么我们应该取通过a,b,c三者修改的最小值作为当前dp[i][j]的value. 

(图片来自官网)

现在递推公式已经有了,可以开始进行编程。

        for (int i = 1; i <= length1; i++) {
            for (int j = 1; j <= length2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1]);
                } else {
                    // 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
            }
        }

(i=0时候代表wordA长度为0的时候,在初始矩阵时候已经赋值。)

矩阵的最终的效果图。dp[lengthA][lengthB]就是返回值。

完整代码如下:

    public static int minDistance(String word1, String word2) {
        if (word1 == null || word2 == null)
            return 0;
        if (word1.length() == 0)
            return word2.length();
        if (word2.length() == 0) {
            return word1.length();
        }
        int length1 = word1.length();
        int length2 = word2.length();
        int[][] dp = new int[length1 + 1][length2 + 1];


        for (int i = 0; i <= length2; i++) {
            dp[0][i] = i;  //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
        }
        for (int i = 0; i <= length1; i++) {
            dp[i][0] = i; 
        }
        System.out.println("====打印初始矩阵========");
        System.out.print("  _ ");
        for (int i = 0 ; i < length2;i++){
            System.out.print(word2.charAt(i)+" ");
        }
        System.out.println();
        for (int i = 0; i <= word1.length(); i++) {
            if (i==0) {
                System.out.print("_ ");
            }else {

            System.out.print(word1.charAt(i-1)+" ");
            }
            for (int j = 0; j <= word2.length(); j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("====打印结束========");


        for (int i = 1; i <= length1; i++) {
            for (int j = 1; j <= length2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]) + 1, dp[i - 1][j - 1]);
                } else {
                    // 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
            }
        }

        System.out.println();
        System.out.println("====打印最终矩阵========");
        System.out.print("  _ ");
        for (int i = 0 ; i < length2;i++){
            System.out.print(word2.charAt(i)+" ");
        }
        System.out.println();
        for (int i = 0; i <= word1.length(); i++) {
            if (i==0) {
                System.out.print("_ ");
            }else {

                System.out.print(word1.charAt(i-1)+" ");
            }
            for (int j = 0; j <= word2.length(); j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("======打印结束======");

        return dp[length1][length2];
    }

那么我们思考一下,如何来优化一下空间,O(n^2)(n^2)的空间还是有些大。

当前位置(i,j)只和(i-1,j),(i,j-1),(i,j)有关系。

那么求值时候,只需要直到当前位置的左,左上,上这三个位置就可以得到当前的值。

于是,只需要一个一维数组+几个变量就可以完全搞定这个问题。

    public static int minDistance2(String word1, String word2) {
        if (word1 == null || word2 == null)
            return 0;
        if (word1.length() == 0)
            return word2.length();
        if (word2.length() == 0) {
            return word1.length();
        }
        int length1 = word1.length();
        int length2 = word2.length();
        int[] dp = new int[length2 + 1];

        for (int i = 0; i <= length2; i++) {
            dp[i] = i;  //初始化
        }
        for (int i = 1; i <= length1; i++) {
            // 记录左上角的值
            int leftUp = dp[0];
            dp[0] = i;
            for (int j = 1; j <= length2; j++) {
                int up = dp[j];
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // j-1代表的是当前的左边的值,是之前的dp[i][j-1]
                    // dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
                    // leftUp代表的是i-1,j-1
                    dp[j] = Math.min(Math.min(dp[j - 1], dp[j]) + 1, leftUp);
                } else {
                    dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftUp) + 1;
                }
                leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
            }
        }
        return dp[length2];
    }

现在的空间优化的其实还不是最好的情况,还可以继续进行空间的优化。

因为刚才dp[lengthB]时候,是按行就行的生成的dp;

也可以按列进行生成,只需要将dp的长度设置为lengthA即可。

于是,dp的长度可以是min(lengthA,lengthB)

所以有如下代码

    public static int minDistance2(String word1, String word2) {
        if (word1 == null || word2 == null)
            return 0;
        if (word1.length() == 0)
            return word2.length();
        if (word2.length() == 0) {
            return word1.length();
        }

        int length1 = word1.length();
        int length2 = word2.length();
        int minLength, maxLength;
        String minWord, maxWord;
        if (length1 > length2) {
            minLength = length2;
            maxLength = length1;
            minWord = word2;
            maxWord = word1;
        } else {
            minLength = length1;
            maxLength = length2;
            minWord = word1;
            maxWord = word2;
        }

        int[] dp = new int[minLength + 1];

        for (int i = 0; i <= minLength; i++) {
            dp[i] = i;  //初始化
        }
        for (int i = 1; i <= maxLength; i++) {
            // 记录左上角的值
            int leftUp = dp[0];
            dp[0] = i;
            for (int j = 1; j <= minLength; j++) {
                int up = dp[j];
                if (maxWord.charAt(i - 1) == minWord.charAt(j - 1)) {
                    // j-1代表的是当前的左边的值,是之前的dp[i][j-1]
                    // dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
                    // leftUp代表的是i-1,j-1
                    dp[j] = Math.min(Math.min(dp[j - 1], dp[j]) + 1, leftUp);
                } else {
                    dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftUp) + 1;
                }
                leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
            }
        }
        return dp[minLength];
    }

 

 

那么已经到了现在这一步了,我们是不是还可以增加一点难度呢?

请看题目:https://www.nowcoder.com/practice/dfa502cf6a914fb5b98c59c56619e96c?tpId=101&&tqId=33111&rp=1&ru=/ta/programmer-code-interview-guide&qru=/ta/programmer-code-interview-guide/question-ranking

备注:

时间复杂度O(n*m)O(n∗m),空间复杂度O(n)O(n)。(n,m代表两个字符串长度)

稍后更。

这个题目相较于上一题主要难在需要弄清楚

[i][j-1],[i-1][j],[i-1][j-1]位置与ic,dc,rc的对应关系。

用一个例子来说明,

ic=5,dc=3,rc=100,wordA=abc,wordB=abc,则过程如下图

    public static long minDistance3(String word1, String word2, int ic, int dc, int rc) {

        if (word1 == null || word2 == null)
            return 0;

        int length1 = word1.length();
        int length2 = word2.length();
        int[][] dp = new int[length1 + 1][length2 + 1];

        for (int i = 0; i <= length2; i++) {
            dp[0][i] = i * ic;  //wordA长度为0的时候,想要到达wordB,只需要增加i个字母
        }
        for (int i = 0; i <= length1; i++) {
            dp[i][0] = i * dc;  // wordB长度为0的时候,想要到达wordA,只需要add增加i个字母
        }

        for (int i = 1; i <= length1; i++) {
            for (int j = 1; j <= length2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 如果当前字符相等,那么如果使用i-1,j-1这条路就不需要进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j] + dc, dp[i][j - 1] + ic), dp[i - 1][j - 1]);
                } else {
                    // 如果当前字符不相等,那么走i-1,j-1时候也需要对最后一个字符进行操作
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j] + dc, dp[i][j - 1] + ic), dp[i - 1][j - 1] + rc);
                }
            }
        }
        return dp[length1][length2];
    }

按之前的思路压缩空间之后的效果是:

public static int minDistance4(String word1, String word2, int ic, int dc, int rc) {
        if (word1 == null || word2 == null)
            return 0;
        
        int length1 = word1.length();
        int length2 = word2.length();
        int minLength, maxLength;
        String minWord, maxWord;
        if (length1 > length2) {
            minLength = length2;
            maxLength = length1;
            minWord = word2;
            maxWord = word1;
        } else {
            minLength = length1;
            maxLength = length2;
            minWord = word1;
            maxWord = word2;
            // 现在是wordB代表的是行,问题转换成了从wordB需要多少代价得到wordA
            // 所以需要交换插入和删除的代价
            int t = ic;
            ic = dc;
            dc = t;
        }

        int[] dp = new int[minLength + 1];

        for (int i = 0; i <= minLength; i++) {
            dp[i] = i * ic;  //初始化
        }
        for (int i = 1; i <= maxLength; i++) {
            // 记录左上角的值
            int leftUp = dp[0];
            dp[0] = i * dc;
            for (int j = 1; j <= minLength; j++) {
                int up = dp[j];
                if (maxWord.charAt(i - 1) == minWord.charAt(j - 1)) {
                    // j-1代表的是当前的左边的值,是之前的dp[i][j-1]
                    // dp[j] 代表的是当前的上边的值,是之前的dp[i-1][j]
                    // leftUp代表的是i-1,j-1
                    dp[j] = Math.min(Math.min(dp[j - 1] + ic, dp[j] + dc), leftUp);
                } else {
                    dp[j] = Math.min(Math.min(dp[j - 1] + ic, dp[j] + dc), leftUp + rc);
                }
                leftUp = up; // j向后移动,那么j+1的左上角就是j被修改之前的j的值
            }
        }
        return dp[minLength];
    }

参考:

https://www.nowcoder.com/practice/dfa502cf6a914fb5b98c59c56619e96c?tpId=101&&tqId=33111&rp=1&ru=/ta/programmer-code-interview-guide&qru=/ta/programmer-code-interview-guide/question-ranking

https://leetcode-cn.com/problems/edit-distance/

https://leetcode-cn.com/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/

 

如有问题,欢迎留言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值