• 代码随想录day56|583. 两个字符串的删除操作|72. 编辑距离|编辑距离总结篇|Golang


    代码随想录day56

    考试周了解一下

    目录

    代码随想录day56

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

    72. 编辑距离

    动态规划之编辑距离总结篇


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

     

    思路

    动态规划

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

            这次是两个字符串可以相互删了,这种题目也知道用动态规划的思路来解,动规五部曲,分析如下:

    1、确定dp数组以及下标的含义

            dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

            这里dp数组的定义有点点绕,大家要撸清思路。

    2、确定递推公式

    • 当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});

            因为dp[i - 1][j - 1] + 1等于 dp[i - 1][j] 或 dp[i][j - 1],所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);

    3、dp数组如何初始化

            从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。

            dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。

    dp[0][j]的话同理,所以代码如下:

    1. vector<vector<int>> dp(word1.size() + 1, vector(word2.size() + 1));
    2. for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
    3. for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;

    4、确定遍历顺序

            从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据左上方、正上方、正左方推出来的。

            所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。

    5、举例推导dp数组

            以word1:"sea",word2:"eat"为例,推导dp数组状态图如下:

     以上分析完毕,代码如下:

    1. func minDistance(word1 string, word2 string) int {
    2. dp := make([][]int, len(word1)+1)
    3. for i := 0; i < len(dp); i++ {
    4. dp[i] = make([]int, len(word2)+1)
    5. }
    6. //初始化
    7. for i := 0; i < len(dp); i++ {
    8. dp[i][0] = i
    9. }
    10. for j := 0; j < len(dp[0]); j++ {
    11. dp[0][j] = j
    12. }
    13. for i := 1; i < len(dp); i++ {
    14. for j := 1; j < len(dp[i]); j++ {
    15. if word1[i-1] == word2[j-1] {
    16. dp[i][j] = dp[i-1][j-1]
    17. } else {
    18. dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+2)
    19. }
    20. }
    21. }
    22. return dp[len(dp)-1][len(dp[0])-1]
    23. }
    24. func min(a, b int) int {
    25. if a < b {
    26. return a
    27. }
    28. return b
    29. }

    72. 编辑距离

             编辑距离终于来了,这道题目如果大家没有了解动态规划的话,会感觉超级复杂。

            编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离。

    接下来我依然使用动规五部曲,对本题做一个详细的分析:

    1. 确定dp数组(dp table)以及下标的含义

            dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

            这里在强调一下:为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢?        

            用i来表示也可以! 但我统一以下标i-1为结尾的字符串,在下面的递归公式中会容易理解一点。

    2. 确定递推公式

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

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

    也就是如上4种情况。

            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删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。

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

            操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。

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

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

    word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a"word1删除元素'd'word2添加一个元素'd',变成word1="a", word2="ad", 最终的操作数是一样! dp数组如下图所示意的:

    1. a a d
    2. +-----+-----+ +-----+-----+-----+
    3. | 0 | 1 | | 0 | 1 | 2 |
    4. +-----+-----+ ===> +-----+-----+-----+
    5. a | 1 | 0 | a | 1 | 0 | 1 |
    6. +-----+-----+ +-----+-----+-----+
    7. d | 2 | 1 |
    8. +-----+-----+

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

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

    递归公式代码如下

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

    3. dp数组如何初始化

            再回顾一下dp[i][j]的定义:

            dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

            那么dp[i][0] 和 dp[0][j] 表示什么呢?

    dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。

    那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;

    同理dp[0][j] = j;

    所以C++代码如下:

    1. for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
    2. for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;

    4. 确定遍历顺序

    从如下四个递推公式:

    • dp[i][j] = dp[i - 1][j - 1]
    • dp[i][j] = dp[i - 1][j - 1] + 1
    • dp[i][j] = dp[i][j - 1] + 1
    • dp[i][j] = dp[i - 1][j] + 1

    可以看出dp[i][j]是依赖左方,上方和左上方元素的,如图:

    所以在dp矩阵中一定是从左到右从上到下去遍历。 

    1. for (int i = 1; i <= word1.size(); i++) {
    2. for (int j = 1; j <= word2.size(); j++) {
    3. if (word1[i - 1] == word2[j - 1]) {
    4. dp[i][j] = dp[i - 1][j - 1];
    5. }
    6. else {
    7. dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
    8. }
    9. }
    10. }

    5. 举例推导dp数组

    以示例1为例,输入:word1 = "horse", word2 = "ros"为例,dp矩阵状态图如下:

     

    1. func minDistance(word1 string, word2 string) int {
    2. m, n := len(word1), len(word2)
    3. dp := make([][]int, m+1)
    4. for i := range dp {
    5. dp[i] = make([]int, n+1)
    6. }
    7. for i:=0;i1;i++{
    8. dp[i][0] = i // word1[i] 变成word2[0],删除word1[i]需要i步操作
    9. }
    10. for j:=0;j1;j++{
    11. dp[0][j] = j // word1[0] 变成word2[j], 插入word1[j]需要j步操作
    12. }
    13. for i:=1;i1;i++{
    14. for j:=1;j1;j++{
    15. if word1[i-1] == word2[j-1] {
    16. dp[i][j] = dp[i-1][j-1]
    17. } else { // min(插入、删除、替换)
    18. dp[i][j] = Min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
    19. }
    20. }
    21. }
    22. return dp[m][n]
    23. }
    24. func Min(args ...int) int {
    25. min := args[0]
    26. for _, item := range args {
    27. if item < min {
    28. min = item
    29. }
    30. }
    31. return min
    32. }

    动态规划之编辑距离总结篇

    本周我们讲了动态规划之终极绝杀:编辑距离,为什么叫做终极绝杀呢?

    细心的录友应该知道,我们在前三篇动态规划的文章就一直为 编辑距离 这道题目做铺垫

    判断子序列

    动态规划:392.判断子序列给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

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

    • if (s[i - 1] == t[j - 1])
      • t中找到了一个字符在s中也出现了
    • if (s[i - 1] != t[j - 1])
      • 相当于t要删除元素,继续匹配

    状态转移方程:

    1. if (s[i - 1] == t[j - 1]) {
    2. dp[i][j] = dp[i - 1][j - 1] + 1
    3. } else {
    4. dp[i][j] = dp[i][j - 1]
    5. }

    不同的子序列

    动态规划: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];

    状态转移方程:

    1. if (s[i - 1] == t[j - 1]) {
    2. dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
    3. } else {
    4. dp[i][j] = dp[i - 1][j];
    5. }

    两个字符串的删除操作

          动态规划: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});

    状态转移方程:

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

    编辑距离

    动态规划:72.编辑距离

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

    编辑距离终于来了,有了前面三道题目的铺垫,应该有思路了,本题是两个字符串可以增删改,比 动态规划:判断子序列

    动态规划:不同的子序列动态规划:两个字符串的删除操作都要复杂的多

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

    • 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;

    递归公式代码如下:

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

    总结

    心思的录友应该会发现我用了三道题做铺垫,才最后引出了动态规划:72.编辑距离

  • 相关阅读:
    JDK 21的新特性总结和分析
    【vscode编辑器插件】前端 php unity自用插件分享
    linux控制组: cpuset解析
    大数据工程师的日常工作内容是干嘛?
    C#正则表达式总结
    计算机毕业设计之java+springboot基于vue的地方美食分享网站
    【Python深度学习】深度学习入门介绍
    搭建国外服务器
    Tomcat
    彻底理解零拷贝技术
  • 原文地址:https://blog.csdn.net/weixin_42161901/article/details/127890280