代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结

简介: 代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结

代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结

文章链接:两个字符串的删除操作编辑距离编辑距离总结

视频链接:两个字符串的删除操作编辑距离

1. LeetCode 583. 两个字符串的删除操作

1.1 思路

  1. 题目给我们两个字符串问两个字符串最少可以删除多少个元素使这两个字符串相同。本题相对于115. 不同的子序列难在于两个都可以删
  2. dp 数组及其下标的含义:比较两个字符串里的每个元素是否相同的情况就定义二维数组,dp[i][j]:以 i-1 为结尾的 word1 和以 j-1 为结尾的 word2,为了让它们相同的最少操作次数为 dp[i][j]。定义 i-1 和 j-1 是为了方便初始化
  3. 递推公式:if(word1[i-1]==word2[j-1])dp[i][j]=dp[i-1][j-1] 因为这两个元素已经相同了,考虑与不考虑都是一样的,因此直接继承dp[i-1][j-1],因为两个元素都相同了就可以不操作它们了。else 就是word1[i-1]!=word2[j-1] 的情况就需要删除元素了,如果删除word1[i-1],dp[i][j]=dp[i-1][j]+1 这里就是模拟出了删除 word1[i-1];如果删除word2[j-1],dp[i][j]=dp[i][j-1]+1 这里就是模拟出了删除 word2[j-1];如果都删了就是 dp[i][j]=dp[i-1][j-1]+2。因此如果两个元素不相同就是 dp[i][j]=Math.min(三个取最小值)
  4. dp 数组的初始化:根据递推公式得到推导方向,因此第一行和列都需要初始化,dp[0][j] 表示以-1 为结尾即空字符串 word1,要想和 word2 相同的最小操作次数就应该是 j,因为 word2 就需要删掉 j 个元素;dp[i][0] 表示以-1 为结尾即空字符串 word2,要想和 word1 相同的最小操作次数就应该是 1,因为 word1 就需要删掉 i 个元素。其余下标因为会被覆盖,因此默认即可
  5. 遍历顺序:从推导方向可以看出就是从左到右从上到下, for(int i=1;i<=word1.length();i++)for(int j=1;word2<=t.length();j++),为什么从 1 开始?为什么要等于号?以前解释过了。最终结果保存在 dp[word1.length()][word2.length()]
  6. 打印 dp 数组:用于 debug
  7. 其实本题也可以通过求1143. 最长公共子序列一样,求出最长公共子序列,然后两个原字符串总和相加减去 2 倍的最长公共子序列的长度就是要求的最小操作次数

.2 代码

// dp数组中存储需要删除的字符个数
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;
        for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;
        
        for (int i = 1; i < word1.length() + 1; i++) {
            for (int j = 1; j < word2.length() + 1; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }else{
                    dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
                                        Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
                }
            }
        }
        
        return dp[word1.length()][word2.length()];
    }
}
//DP - longest common subsequence (用最長公共子序列反推)
class Solution {
    public int minDistance(String word1, String word2) {
        char[] char1 = word1.toCharArray();
        char[] char2 = word2.toCharArray();

        int len1 = char1.length;
        int len2 = char2.length;

        int dp[][] = new int [len1 + 1][len2 + 1];

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

        return len1 + len2 - (2 * dp[len1][len2]);//和leetcode 1143只差在這一行。
    }
}

2. LeetCode 72. 编辑距离

2.1 思路

  1. 本题是给我们两个字符串,如何让 word1 和 word2 相等,可以通过添加、删除、替换元素,对 word1 和 word2 操作都可以,增加 word1 的元素也就是相当于删除 word2 的元素,因此操作方式很多样
  2. dp 数组及其下标的含义:比较两个字符串的相同情况我们要定义一个二维数组,dp[i][j]:以 i-1 为结尾的 word1 和以 j-1 为结尾的 word2 最少操作次数为 dp[i][j] 能使两个字符串相等。为什么以 i-1 和 j-1 结尾是为了方便初始化
  3. 递推公式:要求相同就要比较每个元素,讨论它们相同与不相同的情况 if(word1[i-1]==word2[j-1])dp[i][j]=dp[i-1][j-1] 因为两个元素都相同了,就可以不改变操作次数,不需要任何操作。else 就是两个元素不相同,有增加、删除、替换等操作,如果删掉 word1 的第 i-1 个元素,那就是 dp[i][j]=dp[i-1][j]+1,也就是以 i-2 为结尾和以 j-1 为结尾的相同的最小操作数的基础上+1,如果删掉 word2 的第 j-1 个元素,dp[i][j]=dp[i][j-1]+1,如果两个元素不相同选择替换元素,无论是替换 word1 的还是 word2 的,把其中一个的替换成另一个的就可以了,因此是 dp[i][j]=dp[i-1][j-1]+1,这里含义是把 word1 的第 i-1 个元素换成了 word2 的第 j-1 个元素或者 word2 的第 j-1 个元素换成了 word1 的第 i-1 个元素。因此 dp[i][j]=Math.min(三者取最小值)。为什么这里没有添加元素的操作呢?因为对 word1 添加其实就相当于对 word2 删除,反之也是同理,因此我们用删除代替了添加,效果是一样的 80024e8527a6463d923a01ba038d5473.png
  4. dp 数组的初始化:根据递推公式得到推导方向,因此我们要初始化第一行和列都初始化了,dp[i][0],也就是以 i-1 为结尾的字符串 word1 和以-1 为结尾的空字符串 word2,word1 要最少操作 i 次才能与 word2 相等,因此 for(int i=0;i<=word1.length();i++)dp[i][0]=i;dp[0][j],以-1 为结尾的空字符串 word1 和以 j-1 为结尾的字符串 word2,word2 最少操作 j 次才能与 word1 相等,因此 for(int j=0;j<=word2.length();j++)dp[0][j]=j。为什么取等于号,因为 dp是以 i-1 为结尾和 j-1 为结尾的,一定要取等了,才能表示最后一个元素为结尾。其余下标无所谓,会被覆盖,默认为 0 即可。
  5. 遍历顺序:根据递推公式和推导方向,就一定是从左到右和从上到下,for(int i=1;i<=word1.length();i++)for(int j=1;word2<=t.length();j++),为什么从 1 开始?为什么取等于号?上面解释过了。最终结果保存在 dp[word1.length()][word2.length()]
  6. 打印 dp 数组:用于 debug

2.2 代码

public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 初始化
    for (int i = 1; i <= m; i++) {
        dp[i][0] =  i;
    }
    for (int j = 1; j <= n; j++) {
        dp[0][j] = j;
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 因为dp数组有效位从1开始
            // 所以当前遍历到的字符串的位置为i-1 | j-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }
        }
    }
    return dp[m][n];
}

3. 编辑距离总结

本题是通过三道题铺垫而来的

3.1 392. 判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

这道题目 其实是可以用双指针或者贪心的的,但是我在开篇的时候就说了这是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。

  • if (s[i - 1] == t[j - 1])
  • t中找到了一个字符在s中也出现了
  • if (s[i - 1] != t[j - 1])
  • 相当于t要删除元素,继续匹配
    状态转移方程:
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];

3.2 115. 不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

本题虽然也只有删除操作,不用考虑替换增加之类的,但相对于392. 判断子序列就有难度了,这道题目双指针法可就做不了。

当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。

一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。

一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。

这里可能有同学不明白了,为什么还要考虑 不用s[i - 1]来匹配,都相同了指定要匹配啊。

例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。

所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]

所以递推公式为:dp[i][j] = dp[i - 1][j];

状态转移方程:

if (s[i - 1] == t[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
    dp[i][j] = dp[i - 1][j];
}

3.3 583. 两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步数,每步可以删除任意一个字符串中的一个字符。

本题和115. 不同的子序列相比,其实就是两个字符串可以都可以删除了,情况虽说复杂一些,但整体思路是不变的。

  • 当word1[i - 1] 与 word2[j - 1]相同的时候
  • 当word1[i - 1] 与 word2[j - 1]不相同的时候

当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];

当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:

情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1

情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1

情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2

那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});

状态转移方程:

if (word1[i - 1] == word2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1];
} else {
    dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
}

3.4 72. 编辑距离

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

编辑距离终于来了,有了前面三道题目的铺垫,应该有思路了,本题是两个字符串可以增删改,比392. 判断子序列、115. 不同的子序列、583. 两个字符串的删除操作要复杂很多

在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:

  • if (word1[i - 1] == word2[j - 1])
  • 不操作
  • if (word1[i - 1] != word2[j - 1])

也就是如上四种情况。

if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];

此时可能有同学有点不明白,为啥要即dp[i][j] = dp[i - 1][j - 1]呢?

那么就在回顾上面讲过的dp[i][j]的定义,word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1] 就是 dp[i][j]了。

在下面的讲解中,如果哪里看不懂,就回想一下dp[i][j]的定义,就明白了。

在整个动规的过程中,最为关键就是正确理解dp[i][j]的定义!

if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?

操作一:word1增加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-2为结尾的word1 与 i-1为结尾的word2的最近编辑距离 加上一个增加元素的操作。

即 dp[i][j] = dp[i - 1][j] + 1;

操作二:word2添加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个增加元素的操作。

即 dp[i][j] = dp[i][j - 1] + 1;

这里有同学发现了,怎么都是添加元素,删除元素去哪了。

word2添加一个元素,相当于word1删除一个元素,例如 word1 = “ad” ,word2 = “a”,word2添加一个元素d,也就是相当于word1删除一个元素d,操作数是一样!

操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。

即 dp[i][j] = dp[i - 1][j - 1] + 1;

综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;

递归公式代码如下:

if (word1[i - 1] == word2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1];
}
else {
    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}


相关文章
|
26天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
27天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
61 1
|
1月前
|
存储 缓存 算法
通过优化算法和代码结构来提升易语言程序的执行效率
通过优化算法和代码结构来提升易语言程序的执行效率
|
1月前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
1月前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
1月前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
49 3
|
1月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
2月前
|
JavaScript
力扣3333.找到初始输入字符串Ⅱ
【10月更文挑战第9天】力扣3333.找到初始输入字符串Ⅱ
40 1
|
2月前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
2月前
|
搜索推荐
插入排序算法的讲解和代码
【10月更文挑战第12天】插入排序是一种基础的排序算法,理解和掌握它对于学习其他排序算法以及数据结构都具有重要意义。你可以通过实际操作和分析,进一步深入了解插入排序的特点和应用场景,以便在实际编程中更好地运用它。