leetcode-编辑距离--从暴力法到动态规划

 题目来自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')

 首先需要说明一些操作的等价性:

  • 在word1插入==在word2删除,比如word1=xxx,word2 =xxxy,在word1插入y跟在word2删除y是等价的;
  • 在word1删除==在word2插入,同上;
  • 在word1替换==在word2替换,这个很好理解,你把word1中的x替换为y跟将word2中的y替换为x是等价的。

 所以实际上只需要对一个字符串做三种操作就行了,不用对两个字符串都做(否则就有六种操作了)。

 首先是暴力法,罗列三种可能的操作,取最小值返回。具体做法是定义一个函数distance(word1, word2, idx1, idx2)代表当前对比的字符为word1[idx1]word2[idx2],根据这俩字符是否一样有两种情况:

  • 俩字符一样,说明不需要额外操作,直接对比下一个字符,也就是distance(word1, word2, idx1, idx2)=distance(word1, word2, idx1+1, idx2+1)
  • 俩字符不一样,有三种操作可选,我们选择这三种操作中最终操作数较少的那个:
    • word1插入一个字符word2[idx2]使得跟word2[0:idx2]匹配上(从0到idx2这一段已经转换成功),所以下一个对比的字符就是word1[idx1]word2[idx2+1],也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1, idx2+1),这里的1就是插入操作的次数;
    • word1删除一个字符word1[idx1],所以下一个对比的字符就是word1[idx1+1]word2[idx2],也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1+1, idx2)
    • word1替换字符word1[idx1]word2[idx2]使得跟word2[0:idx2]匹配上(从0到idx2这一段已经转换成功),所以下一个对比的字符就是word1[idx1+1]word2[idx2+1],也就是distance(word1, word2, idx1, idx2)=1+distance(word1, word2, idx1+1, idx2+1)

 JAVA版代码如下:

class Solution {
    private int distance(String word1, String word2, int idx1, int idx2) {
        if (idx1 == word1.length()) {
            return word2.length() - idx2;
        }
        if (idx2 == word2.length()) {
            return word1.length() - idx1;
        }
        int result = 0;
        if (word1.charAt(idx1) == word2.charAt(idx2)) {
            result = distance(word1, word2, idx1 + 1, idx2 + 1);
        }
        else {
            int d1 = 1 + distance(word1, word2, idx1 + 1, idx2);    //删除word1[idx1]
            int d2 = 1 + distance(word1, word2, idx1, idx2 + 1);    //word1插入一个字符word2[idx2]
            int d3 = 1 + distance(word1, word2, idx1 + 1, idx2 + 1);//word1[idx1]替换为word2[idx2]
            result = Math.min(Math.min(d1, d2), d3);
        }
        return result;
    }
    public int minDistance(String word1, String word2) {
        return distance(word1, word2, 0, 0);
    }
}

 当然暴力法就超时了,因为上面的代码中我们会重复计算同一个(idx1, idx2)的二元组,所以可以做一些剪枝操作,用一个矩阵记录计算过的(idx1, idx2)的值避免重复计算。

 JAVA版代码如下:

class Solution {
    private int len1;
    private int len2;
    private int[][] record;

    private int distance(String word1, String word2, int idx1, int idx2) {
        if (idx1 == len1) {
            return len2 - idx2;
        }
        if (idx2 == len2) {
            return len1 - idx1;
        }
        if (record[idx1][idx2] > 0) {
            return record[idx1][idx2];
        }
        int result = 0;
        if (word1.charAt(idx1) == word2.charAt(idx2)) {
            result = distance(word1, word2, idx1 + 1, idx2 + 1);
        }
        else {
            int d1 = 1 + distance(word1, word2, idx1 + 1, idx2);    //删除word1[idx1]
            int d2 = 1 + distance(word1, word2, idx1, idx2 + 1);    //word1插入一个字符word2[idx2]
            int d3 = 1 + distance(word1, word2, idx1 + 1, idx2 + 1);//word1[idx1]替换为word2[idx2]
            result = Math.min(Math.min(d1, d2), d3);
        }
        record[idx1][idx2] = result;
        return result;
    }

    public int minDistance(String word1, String word2) {
        len1 = word1.length();
        len2 = word2.length();
        record = new int[len1][len2];

        return distance(word1, word2, 0, 0);
    }
}

 提交结果如下:


 最后是动态规划的方法,其实就是将上面的递归改成递推。假设用一个二维数组dp记录需要的操作数,dp[i][j]表示将word1的前i个字符转换为word2的前j个字符需要的操作数。首先观察初始状态,也就是一个字符串为空,那么很明显,需要的操作数就是另一个字符串的长度(插入/删除的次数),也就是dp[0][j]=j以及dp[i][0]=i。然后需要递推公式,同样需要分情况:

扫描二维码关注公众号,回复: 10820014 查看本文章
  • word1[i]==word2[j]dp[i][j]=dp[i-1][j-1],因为word1[i]word2[j]一样,所以不需要额外操作即可完成转换,需要的操作数等于将word1的前i-1个字符转换为word2的前j-1个字符需要的操作数;
  • word1[i]!=word2[j]dp[i][j]=1+min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]),因为word1[i]word2[j]不一样,所以现在有三种操作(我们选择这三个中需要操作数最少的那个):
    • word1在第i个字符之后插入字符word2[j],需要的操作数等于将word1的前i个字符转换为word2的前j-1个字符需要的操作数再加上1(即插入操作),也就是1+dp[i][j-1]
    • word1删除第i个字符,需要的操作数等于将word1的前i-1个字符转换为word2的前j个字符需要的操作数再加上1(即删除操作),也就是1+dp[i-1][j]
    • word1第i个字符替换为word2第j个字符,需要的操作数等于将word1的前i-1个字符转换为word2的前j-1个字符需要的操作数再加上1(即替换操作),也就是1+dp[i-1][j-1]

 动态规划方法的时间复杂度为 O ( m n ) O(mn) ,空间复杂度也是 O ( m n ) O(mn) ,其中m、n分别为两个字符串的长度。

 JAVA版代码如下:

class Solution {

    public int minDistance(String word1, String word2) {
        char[] char1 = word1.toCharArray();
        char[] char2 = word2.toCharArray();
        int[][] dp = new int[char1.length + 1][char2.length + 1];

        for (int i = 1; i <= char1.length; ++i) {
            dp[i][0] = i;
        }
        for (int j = 1; j <= char2.length; ++j) {
            dp[0][j] = j;
        }

        for (int i = 1; i <= char1.length; ++i) {
            for (int j = 1; j <= char2.length; ++j) {
                if (char1[i - 1] == char2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                else {
                    dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
                }
            }
        }

        return dp[char1.length][char2.length];
    }
}

 提交结果如下:


 然后进一步考虑到每个状态dp[i][j]都只与dp[i-1][j]dp[i][j-1]以及dp[i-1][j-1]有关,所以可以进一步减少空间复杂度,只用一维数组保存需要的dp[i-1][j]以及dp[i][j-1],单独用一个变量保存dp[i-1][j-1],从而将空间复杂度降低到 O ( n ) O(n)

 JAVA版代码如下:

class Solution {

    public int minDistance(String word1, String word2) {
        char[] char1 = word1.toCharArray();
        char[] char2 = word2.toCharArray();
        int[] dp = new int[char2.length + 1];

        for (int j = 1; j <= char2.length; ++j) {
            dp[j] = j;
        }

        for (int i = 1; i <= char1.length; ++i) {
            int dp_ij_1 = i - 1;
            dp[0] = i;
            for (int j = 1; j <= char2.length; ++j) {
                int temp = dp[j];
                if (char1[i - 1] == char2[j - 1]) {
                    dp[j] = dp_ij_1;
                }
                else {
                    dp[j] = 1 + Math.min(Math.min(dp[j], dp[j - 1]), dp_ij_1);
                }
                dp_ij_1 = temp;
            }
        }

        return dp[char2.length];
    }
}

 提交结果如下:


 Python版代码如下:

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        len1 = len(word1)
        len2 = len(word2)
        dp = [i for i in range(len2 + 1)]
        
        for i in range(1, len1 + 1):
            dp_ij_1 = dp[0]
            dp[0] = i
            for j in range(1, len2 + 1):
                temp = dp[j]
                if word1[i - 1] == word2[j - 1]:
                    dp[j] = dp_ij_1
                else:
                    dp[j] = 1 + min(dp[j - 1], dp[j], dp_ij_1)
                dp_ij_1 = temp
        return dp[len2]

 提交结果如下:


发布了68 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JR_Chan/article/details/105346150
今日推荐