代码随想录算法训练营第五十五天 | 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;
}


相关文章
|
10天前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
21天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
24 3
|
20天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
26天前
|
缓存 分布式计算 监控
优化算法和代码需要注意什么
【10月更文挑战第20天】优化算法和代码需要注意什么
18 0
|
28天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
13天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
14天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
15天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。
|
14天前
|
机器学习/深度学习 算法 芯片
基于GSP工具箱的NILM算法matlab仿真
基于GSP工具箱的NILM算法Matlab仿真,利用图信号处理技术解析家庭或建筑内各电器的独立功耗。GSPBox通过图的节点、边和权重矩阵表示电气系统,实现对未知数据的有效分类。系统使用MATLAB2022a版本,通过滤波或分解技术从全局能耗信号中提取子设备的功耗信息。
|
14天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
33 3