代码随想录算法训练营第45天 | 第九章 动态规划part12:115.不同的子序列、583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结篇

第九章 动态规划part12

115.不同的子序列

但相对于刚讲过 392.判断子序列,本题 就有难度了 ,感受一下本题和 392.判断子序列 的区别。
文章讲解

思路

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。

  2. 确定递推公式
    这一类问题,基本是要分析两种情况

  • s[i - 1] 与 t[j - 1]相等
  • s[i - 1] 与 t[j - 1] 不相等
  • 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。
    一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 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]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]
    所以递推公式为:dp[i][j] = dp[i - 1][j];
  1. dp数组如何初始化
    从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,如图:,那么 dp[i][0] 和dp[0][j]是一定要初始化的。
    每次当初始化的时候,都要回顾一下dp[i][j]的定义,不要凭感觉初始化。
  • dp[i][0]表示什么呢?
    dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
    那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。
  • 再来看dp[0][j],dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
    那么dp[0][j]一定都是0,s如论如何也变成不了t。
  • 最后就要看一个特殊位置了,即:dp[0][0] 应该是多少。
    dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
  1. 确定遍历顺序
    从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的。

  2. 举例推导dp数组
    以s:"baegg",t:"bag"为例,推导dp数组状态如下:


    image.png
class Solution {
    public int numDistinct(String s, String t) {
        int len1 = s.length();
        int len2 = t.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 0; i < len1; i++) dp[i][0] = 1;
        for(int j = 1; j < len2; j++) dp[0][j] = 0;
        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; j++){
                if(s.charAt(i - 1) == t.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[len1][len2];
    }
}

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

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

思路

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

  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][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
      从字面上理解 就是 当 同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[i][j-1] + 1。
  1. dp数组如何初始化
    从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。
    dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。
    dp[0][j]的话同理。

  2. 确定遍历顺序
    从递推公式 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]可以根据之前计算出来的数值进行计算。

  3. 举例推导dp数组

    以word1:"sea",word2:"eat"为例,推导dp数组状态图如下:
    image.png
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 0; i <= len1; i++) dp[i][0] = i;
        for(int j = 0; j <= len2; j++) dp[0][j] = j;
        for(int i = 1; i <= len1; i++){
            for(int j = 1; j <= len2; 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, dp[i][j - 1] + 1);               
                }
            }
        }
        return dp[len1][len2];
    }
}

动态规划二

本题和 动态规划:1143.最长公共子序列 基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。

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

        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]);
    }
}

72. 编辑距离

最终我们迎来了编辑距离这道题目,之前安排题目都是为了 编辑距离做铺垫。
文章讲解

思路

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。

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

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

  • 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", 最终的操作数是一样的。

  • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。
    可以回顾一下,if (word1[i - 1] == word2[j - 1])的时候我们的操作 是 dp[i][j] = dp[i - 1][j - 1]
    那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。
    所以 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. 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;
  1. 确定遍历顺序
    从如下四个递推公式:
    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]是依赖左方,上方和左上方元素的,如图:
    image.png
  2. 举例推导dp数组
    以示例1为例,输入:word1 = "horse", word2 = "ros"为例,dp矩阵状态图如下:


    image.png
class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        char[] char1 = word1.toCharArray();
        char[] char2 = word2.toCharArray();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for(int i = 0; i <= len1; i++) dp[i][0] = i;
        for(int j = 0; j <= len2; j++) dp[0][j] = j;
        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];
                }else{
                    dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j - 1]), dp[i-1][j-1]) + 1;
                }
            }
        }
        return dp[len1][len2];
    }
}

编辑距离总结篇

做一个总结吧

https://programmercarl.com/%E4%B8%BA%E4%BA%86%E7%BB%9D%E6%9D%80%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%8C%E5%8D%A1%E5%B0%94%E5%81%9A%E4%BA%86%E4%B8%89%E6%AD%A5%E9%93%BA%E5%9E%AB.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容