当前位置: 首页 > article >正文

【ONE·基础算法 || 动态规划(三)】

在这里插入图片描述

总言

  主要内容:编程题举例,熟悉理解动态规划类题型(回文串问题、两个数组的 dp问题)。
  
  
  
  
  

文章目录

  • 总言
  • 7、回文串问题
    • 7.1、 回文子串(medium)
      • 7.1.1、题解
    • 7.2、 最长回文子串(medium)
      • 7.2.1、中心扩散法
      • 7.2.2、动态规划法
    • 7.3、分割回文串 IV(hard)
      • 7.3.1、题解
    • 7.4、分割回文串II(hard)
      • 7.4.1、题解
    • 7.5、最长回文子序列(medium)
      • 7.5.1、题解
    • 7.6、让字符串成为回文串的最小插入次数(hard)
      • 7.6.1、题解
  • 8、两个数组的 dp
    • 8.1、最长公共子序列(medium)
      • 8.1.1、题解
    • 8.2、不相交的线(medium)
      • 8.2.1、题解
    • 8.3、 不同的子序列(hard)
      • 8.3.1、题解
    • 8.4、通配符匹配(hard)
      • 8.4.1、题解
    • 8.5、正则表达式匹配(hard)
      • 8.5.1、题解
    • 8.6、 交错字符串(medium)
      • 8.6.1、题解
    • 8.7、两个字符串的最小ASCII删除和(medium)
      • 8.7.1、题解
    • 8.8、 最长重复子数组(medium)
      • 8.8.1、题解
  • Fin、共勉。

  
  
  
  
  

7、回文串问题

7.1、 回文子串(medium)

  题源:链接。

在这里插入图片描述
  
  

7.1.1、题解

  1)、说明
  做法有很多,比如:
  中心扩展算法,时空复杂度分别为 O ( n 2 ) 、 O ( 1 ) O(n^2)、O(1) O(n2)O(1)
  马拉车算法,时空复杂度分别为 O ( n ) 、 O ( n ) O(n)、O(n) O(n)O(n)
  这里我们主要学习动态规划的思想,时空复杂度分别为 O ( n 2 ) 、 O ( n 2 ) O(n^2)、O(n^2) O(n2)O(n2)虽然相对于其它两种解法而言,动态规划的方法不是最优解,但其能够将所有的子串是否是回文的信息,保存在dp表中。这在一些题中起到化繁为简的效果。

  
  
  
  2)、思路分析

  1、确定状态表示:
  如何将所有子串是否是回文的信息统计在dp表中?首先得找到所有的子串才行。从最直观的暴力解法来看,这需要我们使用两层循环,分别用于定位子串的起始字符 i 和结尾字符 j ,通过这样的遍历方式,我们能够捕捉到字符串中的每一个子串。
  我们获得这些子串的目的,是为了判断它们是否为回文子串。(i,j)位置帮助我们定位了某一个具体的子串,那么我们可以使用一个n×n的二维数组,这样dp[i][j]就用于记录这个 以 i 为起点、j 为终点的子串是否为回文子串。即:

dp[i][j]:用于表示s字符串中的(i,j)子串,是否是回文串。规定 i <= j.

在这里插入图片描述

  
  
  2、确定状态转移方程: 对于拿到的一个子串(i,j),我们可以从区间两端分析起

1、若 s[i] != s[j],首尾两元素都不相等,说明这个子串不是回文串。此时有 dp[i][j] = false;
2、若 s[i] == s[j],首尾两元素相等的时候,可以根据⻓度细分为三种情况。
   1)、i == j,长度为1,就一个元素构成子串。根据题目,这必然是回文串,此时有 dp[i][j] = true;
   2)、i + 1 == j,长度为2,就 i、j两个相邻元素构成子串。根据题目,这也是回文串,此时有 dp[i][j] = true;
   3)、子串长度大于2,已经确定s[i] == s[j],此时要判断回文串,需要判断(i+1,j-1)是否构造回文串,也就是 dp[i][j] = dp[i+1][j-1]

在这里插入图片描述
  

  3、初始化: dp表中初始化是为了以防在填表时发生越界,但在本题中,dp表无需初始化,因为我们对 “ i == j 长度为 1 ” 和 “ i +1 == 长度为 2 ” 的情况做了细致处理,所以对于dp[i][j] = dp[i + 1][j - 1]。i+1 和 j-1向内收缩的时候,最后都会被归到上述两种情况中。

在这里插入图片描述

  

  4、填表顺序: 根据状态方程,填写(i,j)位置时,需要依赖(i+1,j-1)位置的元素,因此填表顺序应该是从下往上。每一行的左右填表顺序,无所谓。
  
  5、返回值: 题目要求统计并返回这个字符串中 回文子串 的数目,dp表中保存的是当前 (i,j) 子串是否是回文子串,因此我们需要返回dp表中true的个数
  
  
  

  3)、题解
  

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        
        //1、创建dp表并初始化:无需特殊处理
        vector<vector<bool>> dp(n,vector<bool>(n));

        // 2、填表:从下往上
        int sum = 0;//统计返回值
        for(int i = n - 1; i >=0; --i)
        {
            for(int j = i; j < n; ++j)//注意这里枚举 j 时,j 的起始位置
            {
                if(s[i] == s[j])// 判断(i,j)两端
                    dp[i][j] = (i + 1 < j) ? dp[i+1][j-1] : true;//注意理解这种写法
                if(dp[i][j]) ++sum;
            }
        }
        // 3、返回
        return sum;
    }
};

  
  
  
  
  
  
  
  
  

7.2、 最长回文子串(medium)

  题源:链接。

在这里插入图片描述

  
  

7.2.1、中心扩散法

  「中心扩散法」的基本思想是:遍历每一个下标,以这个下标为中心,利用「回文串」中心对称的特点,往两边扩散,看最多能扩散多远。
  题解链接:字符串。
  
  
  
  

7.2.2、动态规划法

  1)、思路分析

  1、确定状态表示: 本题解题思路和7.1类似。我们定义一个dp表,dp[i][j] 中存储着(i,j)子串是否是回文串的信息。因为已知下标,自然,我们就可以根据下标,得到是回文串的那些子串的长度。len = j - i + 1.
  
  

  2、确定状态转移方程: 对于拿到的一个子串(i,j),我们可以从区间两端分析起

1、若 s[i] != s[j],首尾两元素都不相等,说明这个子串不是回文串。此时有 dp[i][j] = false;
2、若 s[i] == s[j],首尾两元素相等的时候,可以根据⻓度细分为三种情况。
   1)、i == j,长度为1,就一个元素构成子串。根据题目,这必然是回文串,此时有 dp[i][j] = true;
   2)、i + 1 == j,长度为2,就 i、j两个相邻元素构成子串。根据题目,这也是回文串,此时有 dp[i][j] = true;
   3)、子串长度大于2,已经确定s[i] == s[j],此时要判断回文串,需要判断(i+1,j-1)是否构造回文串,也就是 dp[i][j] = dp[i+1][j-1]

在这里插入图片描述
  
  3、初始化: 无需初始化。

  4、填表顺序: 填表dp(i,j)时需要依赖(i+1,j-1)处的位置,因此需要从下往上填表。

  5、返回值: dp里面值为true的情况下,长度最大的子串的起始位置以及长度。
  
  
  
  2)、题解
  实际上,这里dp表相当于在做预处理工作:判断是否是回文串。
  嵌套了两层循环,时间复杂度为: O ( n 2 ) O(n^2) O(n2)。使用了一个二维数组dp,其大小为n x n,空间复杂度为: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();

        // 1、创建dp表并初始化
        vector<vector<bool>> dp(n, vector<bool>(n)); // dp[i][j]: (i,j)子串是否是回文串

        // 2、填表:从下往上
        int begin = 0;
        int maxlen = 0;
        for (int i = n - 1; i >= 0; --i) 
        {
            for (int j = i; j < n; ++j) 
            {
            
                if (s[i] == s[j])
                    dp[i][j] = (i + 1 < j) ? dp[i + 1][j - 1] : true;

                if (dp[i][j] &&(maxlen < j - i + 1)) //(i,j)是回文串,且出现了更大的长度
                    begin = i, maxlen = j - i + 1; // 更新起始位置和长度
            }
        }

        // 3、返回
        return s.substr(begin, maxlen);
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

7.3、分割回文串 IV(hard)

  题源:链接。

在这里插入图片描述

  
  

7.3.1、题解

  1)、思路分析

  根据题目要求,我们需要将给定的字符串 s 分割成三个非空的回文子字符串。为了实现这一目标,可以采用以下策略:
  ①、选择一个分割点 (i, j),其中 0 < i < j < n-1(n 是字符串 s 的长度),将字符串 s 分割成三个部分:[0,i-1][i,j][j+1,n-1]
  ②、接下来,只需要判断这三个子字符串是否都是回文串即可。
在这里插入图片描述

  而判断某一段区间构成的子串是否是字符串,我们在先前两题中已经学过这个方法,可以使用动态规划来完成。相当于,这里的dp表其实是一个辅助工具/预处理过程。

  
  

  2)、题解
  嵌套了两层循环,两次,时间复杂度为: O ( n 2 ) O(n^2) O(n2)。使用了一个二维数组dp,其大小为 n x n,空间复杂度为: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    bool checkPartitioning(string s) {
        int n = s.size();

        // step1:获取一个dp表,表内存储着(i, j)子串是否是回文串的信息
        // 1、创建dp表并初始化
        vector<vector<bool>> dp(n,vector<bool>(n));

        // 2、填表:从下往上
        for(int i = n - 1; i >= 0; --i)
        {
            for(int j = i; j < n; ++j)
            {
                if(s[i] == s[j])
                    dp[i][j] = (i + 1 < j) ? dp[i+1][j-1] : true;
            }
        }

        // step2:根据获取的dp表,判断以(i, j)作为分割点的三个区间内的子串,是否均为回文串。
        for(int i = 1; i < n -1; ++i)// 因为题目给定了 length >=3, 这里为了省去边界判断,直接从  n-1> i >= 1、 n-1 > j >= i开始
        {                            // 也就是前后两端留了一个字符
            for(int j = i; j < n-1; ++j)
            {
                if(dp[0][i-1] && dp[i][j] && dp[j+1][n-1])
                    return true;
            }
        }
        return false;
    }
};

  
  
  
  
  
  
  
  
  
  
  

7.4、分割回文串II(hard)

  题源:链接。

在这里插入图片描述

  
  

7.4.1、题解

  1)、思路分析
  此题的整体思想和 单词拆分 类似。
  
  先来分析题目,要将字符串 s 经过切割后构成回文串,在极端情况下,如果字符串 s 本身不是回文串,那么长度为 n 的字符串 s 可能需要切割 n-1 次,才能将其分割成所有子串都是回文串(即每个字符单独成为一个回文串)。由此性质可知,对于任意有限长度的字符串 s,我们总是能将其切割形成回文串的。(理解这一点很重要,它有助于理解下述状态转移方程的推导)。
在这里插入图片描述

  1、确定状态表示: 要找到最少的分割次数,根据经验+题目要求,以 i 为结尾,则 dp[i] 表示以 0 为起点, i 为结尾的子串中,能构成回文串所需的最小切割次数。
  
  
  2、推导状态转移方程: 对 子串[0,i] 进行切割,可以分为以下两种情况:

1[0,i] 整个子串本身就是回文串,无需进行任何切割,此时dp[i] = 0;
2[0,i] 不构成回文串,需要进行切割。设 j 为切割点,有  0 < j <= i (注意,这里 j 不能为 0,因为0的情况上述已经判断了。j 可以为 i,也就是一个子串本身)
   由此,可将整个子串 [0, i] 分为两部分:[0, j-1][j, i]
   1)、先判断[j,i]是否构成回文串,若不构成,则当前切割点 j 不成立,需要重新寻找。
   2)、[j,i]构成回文串,此时只需要获取[0,j-1]子串中的最小切割次数,那么[0,i]子串中的切割次数,无非是在其后多切一刀。即;dp[i] = dp[j-1]+1.
   PS : 因为 j 切割点可选,而我们需要最小的切割次数,所以我们需要遍历所有可能的 j 值,并选择使 dp[i] 最小的那个。
        因此有 dp[i] = min( dp[i], dp[j-1]+1);

在这里插入图片描述
  优化:这里,为了快速获得某个区间(m,n)是否是回文串,可以借助之前几题的经验,先处理⼀个 dp 表,里面保存所有子串是否回⽂的信息。
  
  
  3、初始化: 一般,填表是为了防止不越界,在本题中,由于限制了 0 < j <= i,那么 dp[i] = dp[j-1]+1 是不会来到 j = 0处的。
  但由于我们后续进行了比较dp[i] = min( dp[i], dp[j-1]+1);,为了比较时, 取值为 0 的干扰,我们将dp表全都初始化为无穷大。

  4、填表顺序: 根据状态转移方程,从左到右填表。

  5、返回值: 根据我们的状态表示,这里需要返回dp[n-1]处的值。
  
  
  2)、题解
  时空复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    int minCut(string s) {
        int n = s.size();

        // 0、准备工作:获取所有子串是否是回文串
        vector<vector<bool>> ispd(n, vector<bool>(n));
        for (int i = n - 1; i >= 0; --i) 
        {
            for (int j = i; j < n; ++j) 
            {
                if (s[i] == s[j])
                    ispd[i][j] = (i + 1 < j) ? ispd[i + 1][j - 1] : true;
            }
        }

        // 1、创建dp表并初始化
        vector<int> dp(n, INT_MAX);
        // 2、填表
        for (int i = 0; i < n; ++i) {
            if (ispd[0][i]) dp[i] = 0; //[0,i] 整个子串本身就是回文串
            else 
            {
                for (int j = 1; j <= i; ++j) 
                {
                    if (ispd[j][i]) //[j,i]构成回文串
                        dp[i] = min(dp[j - 1] + 1, dp[i]);
                }
            }
        }
        // 3、返回
        return dp[n - 1];
    }
};

  
  
  
  
  
  
  
  
  
  
  

7.5、最长回文子序列(medium)

  题源:链接。

在这里插入图片描述

  
  

7.5.1、题解

  1)、思路分析
  需要注意理解这里的“子串”和“子序列”。
在这里插入图片描述

  
  1、确定状态表示: 尝试推导状态表示。
  按照惯例,一般会选择以 i 为结尾,dp[i]表示到达 i 位置时的最长回文子序列的长度。如果以这样的方式定义状态表示,在后续推导状态转移方程时,我们会发现无从下手,因为在只知道“长度”信息的情况下,无非判断一段序列是否是回文序列。
  
  关于此类单个字符串中,研究“回文子序列”,或者“回文子串”等问题, 我们的状态表示研究的对象一般都是选取原字符串中的一段区域[i,j]内部的情况。这里我们继续选取字符串中的一段区域来研究:

dp[i][j]表示: s字符串[i, j]区间内的所有的子序列中,最长的回文子序列的长度。规定 i <= j

  

  2、推导状态转移方程: 既然是在[i,j]区间段内挑选元素,看能否组成回文子序列,那么我们先从 i 、j 首尾两个元素看起。

1、s[i] == s[j],首尾两个元素"相同"时:
   1)、若 i == j,即只有一个元素,此时dp[i][j] = 12)、若 i+1 == j,只有i、j两个相邻元素,此时dp[i][j] = 23)、若 i+1 < j ,那么[i,j]区间上的最长回文子序列,应该是 [i + 1,j - 1] 区间内的那个最长回文子序列,
       在其首尾添加上s[i]和s[j],此时dp[i][j] = dp[i+1][j-1] + 22、s[i] != s[j],首尾两个元素"不相同"时:这表明i、j两个元素不能组成回文,就不能同时添加在一个回文串的左右。
   那么我们就应该让s[i]单独加在一个序列的左边,或者让s[j]单独放在一个序列的右边,看看这两种情况下的最大值:
   1)、单独加入s[i]后的区间在[i, j-1],此时最长的回文序列的长度就是dp[i][j-1];
   2)、单独加入s[j]|后的区间在[i + 1,j],此时最长的回文序列的长度就是dp[i+ 1][j] ;
   取两者的最大值:dp[i][j] = max(dp[i][j - 1],dp[i + 1][j])

在这里插入图片描述

  细节理解:当s[i] != s[j]时,我们不能直接跳到[i+1, j-1],因为这样做会忽略掉可能以s[i]开头或以s[j]结尾的最长回文子序列

在这里插入图片描述

  
  

  3、初始化: 因为对 i == ji + 1 == j两种情况做了特殊处理,且dp表中要求 i <= j,因此不会发生越界行为。不用对dp表内数值做特殊处理。
  

  4、填表顺序: 根据状态转移方程,填写dp[i][j]时,需要依赖其左侧、下册、左下侧三个位置的dp值。因此填表顺序为:从下往上填表,每行从左往右填表。
在这里插入图片描述

  

  5、返回值: 根据状态表示,我们需要返回[0,n -1]区域上的最长回文序列的长度,即dp[0][n - 1]。

  
  
  2)、题解
  

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n= s.size();
        // 1、创建dp表并初始化
        vector<vector<int>> dp(n,vector<int>(n));// dp[i][j]:(i,j)子串中最长的回文子序列长度
        
        // 2、填表:从下往上、从左往右
        for(int i = n-1; i >= 0; --i)
        {
            dp[i][i] = 1;// i==j 时
            for(int j = i+1; j < n; ++j)// j 就可以从i+1开始
            {
                if(s[i] == s[j])
                    dp[i][j] = dp[i+1][j-1] + 2;// 这里的填表得到简化(不用三种情况分别写)
                else 
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
            }
        }

        // 3、返回
        return dp[0][n-1];
    }
};

  
  
  
  
  
  
  
  
  
  
  

7.6、让字符串成为回文串的最小插入次数(hard)

  题源:链接。

在这里插入图片描述

  
  

7.6.1、题解

  1)、思路分析

  1、确定状态表示: 根据经验+题目要求,回文串、回文序列通常会选择一段区间[i,j]来研究。因此状态方程为:

dp[i][j]:让以 i 为起点, j 为终点的子串成为回文串的最小插入次数。

  

  2、推导状态转移方程: 关于回文子序列和回文子串的分析方式,一般比较固定,先选择这段区域的左右端点的字符情况来分析。

1、s[i] == s[j],首尾两个元素"相同",插入次数看区间中间部分:
   1)、若 i == j,只有一个元素时,自成回文,无需插入。此时dp[i][j] = 02)、若 i+1 == j,只有i、j两个相邻元素,同样构成回文,无需插入。此时dp[i][j] = 03)、若 i+1 < j ,那么[i,j]区间上的最小插入次数,取决于 [i + 1,j - 1] 区间内的最小插入次数。即dp[i][j] = dp[i+1][j-1]2、s[i] != s[j],首尾两个元素"不相同"时:这意味着i和j两个字符不能组成回文,需要在其中一边插入一个字符来使其变成回文。
   要么在区间右侧插入一个左端点值s[i],要么在区间左侧插入一个右端点值s[j]。选择其中最小的插入次数:
   1)、在区间j的右侧插入一个s[i],插入次数+1。此时左端点i和该数构成回文,最小插入次数需要看[i+1,j]区间,即dp[i][j] = dp[i + 1][j] + 1 ;
   2)、在区间i的左侧插入一个s[j],插入次数+1。此时右端点j和该数构成回文,最小插入次数需要看[i,j-1]区间,即dp[i][j] = dp[i][j - 1] + 1;
   取两者的最小值:dp[i][j] = min(dp[i][j - 1],dp[i + 1][j]) + 1

在这里插入图片描述

  
  
  3、初始化: 实则本题无需特别初始化。
在这里插入图片描述

  
  4、填表顺序: 根据状态转移方程,填写dp[i][j]时,需要依赖其左侧、下册、左下侧三个位置的dp值。因此填表顺序为:从下往上填表,每行从左往右填表。
在这里插入图片描述

  
  5、返回值: 根据状态表示,我们需要返回[0,n -1]区域上成为回文子串的最少插入次数,因此需要返回dp[0][n - 1]

  
  
  2)、题解
  嵌套了两层循环,时间复杂度为: O ( n 2 ) O(n^2) O(n2)。使用了一个二维数组dp,其大小为n x n,空间复杂度为: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    int minInsertions(string s) {
        int n = s.size();
        // 1、创建dp表并初始化:本题无需特别初始化
        vector<vector<int>> dp(n,vector<int>(n));

        // 2、填表:从上往下(每行),从左往右(每列)
        for(int i = n-1; i >=0; --i)
        {
            dp[i][i] = 0;//其实也可以不用特意拎出来:单独一个字符,自成回文,无需切割
            for(int j = i+1; j < n; ++j)//因为上面处理的j = i的情况,这里 j 直接从 i+1开始
            {
                if(s[i] == s[j])
                    dp[i][j] = dp[i+1][j-1];// 这里i+1 == j的情况也包含在内:其依赖的位置本身就初始化为了0
                else
                    dp[i][j] = min(dp[i+1][j],dp[i][j-1])+1;//两种插入方式取最小
            }
        }
        // 3、返回
        return dp[0][n-1];
    }
};

  
  
  
  
  
  
  
  
  
  
  

8、两个数组的 dp

8.1、最长公共子序列(medium)

  题源:链接。

在这里插入图片描述

  
  

8.1.1、题解

  1)、思路分析
  此题为学习这类两个数组的dp的模板题,理解其中每步细节操作很重要。

  
  1、确定状态表示: 对于两个数组的动态规划,定义状态表的经验就是:
  1)、选取第一个数组中的[0,i]区间,以及第二个数组走的[0,j]区间作为研究对象;
  2)、结合题目要求,定义状态表示。
  
  根据本题题目,状态表示为:

dp[i][j]:以s1数组中的 [0,i] 区间,和s2数组中的 [0,j] 区间为结尾的"所有"子序列中,最长的公共子序列的长度。

  ※、强调: 这里状态表示研究的是[0,i]区间段内所有子序列[0,j]区间段内所有子序列,并非单独的以i为结尾的子序列、以j为结尾的子序列。
  
  

  2、推导状态转移方程: 分析状态转移方程的经验就是根据 “最后一个位置” 的状况,分情况讨论

  对于dp[i][j] ,可以根据s1[i]s2[j]的字符分情况讨论:

  情况1)、s1[i] == s2[j]时: 这意味着s1[0,i]s2[0,j]区间内,最长公共序列的最后一个字符,一定会落在ij下标的元素上。(以下是一个反证,可以略过)

在这里插入图片描述

  基于此,要求当前的最长公共子序列的长度,我们只需要在s1 的[0,i-1]区间和 s2 的 [0,j-1] 区间中找到一个最长的公共子序列,然后再加上当前匹配的字符 s1[i](或 s2[j] ,因为它们是相等的)。
  所以,状态转移方程为:dp[i][j] = dp[i-1][j-1] + 1
  
  
  情况2)、s1[i] != s2[j]时: 在这种情况下,最长公共子序列一定不会同时以s1[i]s2[j]结尾。此时,要找最长公共子序列时,需要考虑下面三种情况:
  ①、在s1的[0,i-1]以及s2的[0,j]区间中寻找最长公共子序列:此时最大长度为dp[i-1][j];
  ②、在s1的[0,i]以及s2的[0,j-1]区间中寻找最长公共子序列:此时最大长度为dp[i][j- 1] ;
  ③、在s1的[0,i-1]以及s2的[0,j-1]区间中寻找最长公共子序列:此时最大长度为dp[i - 1][j - 1]
  求三者中的最大值即可。
在这里插入图片描述

  细节说明: 仔细观察会发现,第三种包含在第一种和第二种情况里面。本题中求的是最大值,并不影响最终结果。如果题目换成求最长公共子序列的个数,则会重复计数,因此只需求前两种情况下的最大值即可。

  
  
  
  
  3、初始化:
  1)、关于字符串的dp问题,空串是有研究意义的,比如s1="",无非表示子序列为空的情况。因此我们将原始dp 表的规模多加上一行和一列,表示空串。
  2)、引入空串的概念之后,会方便我们的初始化。但也要注意下标的映射关系,以及里面的填值要保证后续填表是正确的。
  ①、对下标映射:dp表中引入了虚拟节点表示空串,为了方便,可以在原子串s 中加入空g字符作为占位。如此一来,映射关系就得到了统一。即 s= " " + s1
  ②、填表:在本题中,虚拟节点行表示s1为空串,虚拟节点列表示s2为空串,无论哪一个,这种情况下,最长公共子序列的长度为 0,没有长度。因此第一行和第一列的值初始化为0,即可保证后续填表是正确的。
在这里插入图片描述

  
  

  4、填表顺序: 根据状态转移方程,从上往下填写每一行,从左往右填写每一列。
在这里插入图片描述

  
  5、返回值: 根据状态表示,返回dp[m][n](注意这里的下标,因为引入了虚拟节点表示空串)。
  
  

  2)、题解
  在原表中添加" "空字符,使得下标映射与dp表中一致:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size();
        int n = text2.size();

        // 1、创建dp表并初始化:这里引入了虚拟节点
        vector<vector<int>> dp(m + 1,vector<int>(n + 1,0));

        text1 = " " + text1;// 为了方便后续填表时下标对应
        text2 = " " + text2;

        // 2、填表:从上往下,从左到右
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(text1[i] == text2[j])
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
            }
        }

        // 3、返回
        return dp[m][n];
    }
};

  若不这样写,那么需要注意原表的映射关系:

        // 2、填表:从上往下,从左到右
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(text1[i-1] == text2[j-1])// 注意这里!
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
            }
        }

  
  
  
  
  
  
  
  
  
  

8.2、不相交的线(medium)

  题源:链接。

在这里插入图片描述

  
  

8.2.1、题解

  1)、思路分析
  先来分析题目,题目要求绘制连线时,每条连线不会相交。这意味着,当我们为 nums1[i]nums2[j] 绘制一条连线时,接下来的连线必须在这些元素之后(在各自数组中)寻找相同的元素

  具体来说,如果我们已经为 nums1 中的某个元素 a 和 nums2 中的某个元素 b(且 a == b)绘制了一条连线,那么接下来的连线必须在 nums1 中 a 之后的元素和 nums2 中 b 之后的元素中寻找相同的元素。这一要求保证了连线的独立性,即它们不会相交。

  仔细观察会发现,这一逻辑实际上与寻找两个数组中的最长公共子序列( LCS )问题非常相似。在 LCS 问题中,我们需要在两个数组中找出按相同顺序出现的最长元素序列。这里的“按相同顺序出现”正对应了连线不相交的要求。 因此,我们可以将这个问题转化为在两个数组 nums1 和 nums2 中寻找最长公共子序列的问题。

在这里插入图片描述

  因为8.1中已经详细写过分析,此处不再赘叙。
  

  2)、题解
  

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();

        // 1、建表并初始化:引入虚拟节点
        vector<vector<int>> dp(m+1,vector<int>(n+1));

        // 2、填表:从左往右,从上往下。
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(nums1[i-1] == nums2[j-1])// 注意下标映射关系
                    dp[i][j] = dp[i-1][j-1] +1;
                else
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
            }
        }

        // 3、返回
        return dp[m][n];
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

8.3、 不同的子序列(hard)

  题源:链接。

在这里插入图片描述

  
  

8.3.1、题解

  1)、思路分析
  对于两个数组/字符串之间的dp问题,我们一般的思考方式如下:
  1)、选取第一个字符串的[0,i]区间以及第二个字符串的[0,j]区间当成研究对象,结合题目的要求来定义状态表示;
  2)、然后根据两个区间上 “最后一个位置的字符”,来进行“分类讨论”,从而确定“状态转移方程”。
  我们可以根据上面的策略,解决大部分关于两个字符串之间的dp问题。
  

  1、确定状态表示: 根据上述经验+本题的题目要求,我们选取 t 字符串中的区间 [0, i]s 字符串中的区间 [0, j] 作为研究对象。定义二维数组 dp[i][j],其中:
  dp[i][j] 表示:在 s 字符串的前 [0, j] 个字符所组成的所有子序列中,t 字符串的前 [0, i]字符出现的次数。
在这里插入图片描述

  
  
  2、推导状态转移方程: 要在 s[0, j] 字符串的所有子序列中,找字符串 t[0, i] (要求 t[0, i] 中的所有字符都被包含,且顺序一致)。我们可以根据 “在 s[0, j] 中满足要求的子序列是否包含 s[j]” 这一条件进行分情况讨论:

  1)、 s[0, j] 满足要求的子序列中包含 s[j] 元素: 这种情况只有当 t[i] == s[j] 时才成立。这时,我们可以在状态 dp[i-1][j-1] 中找到所有符合要求的子序列,然后在这些子序列的后面再加上一个字符 s[j]。因此,这种情况下的子序列数量为 dp[i-1][j-1]。(注意:在原序列中加上一个字符,原序列的数目是不变的)
在这里插入图片描述

  2)、s[0, j] 满足要求的子序列中不包含s[j]元素: 这时候,就需要去掉s[j]元素,在s[0, j-1]中,找t[0, i]。这意味着,这种情况下满足条件的子序列数量即 dp[i][j-1]
在这里插入图片描述

  两种情况加起来,dp[i][j] 的结果:dp[i][j] = dp[i-1][j-1](有条件) + dp[i][j-1]
  
  其它:为什么不在 s[0,j]中找t[0,i-1]不满足题意,题目是要求在s的子序列中找t,这说明给定t[0,i][0,i] 中出现的字符都要找全。(这点需要和之前的公共子序列区别开来)
  
  

  3、初始化: 引入虚拟节点初始化。需要注意两点:

  1)、虚拟节点中的需要保证后续填表时,状态转移方程正确。
在这里插入图片描述

  2)、下标映射关系:两种方法。①直接填表,但要注意s、t与dp的下标映射。②对s、t两个字符串分别头插一个占位字符" ",使得s、t、和dp间的下标映射对齐。

  
  4、填表顺序: 根据状态转移方程,从上往下填每一行,每一行左到右。
  
  5、返回值: 根据状态表示,需要返回dp[m][n]。
  
  

  2)、题解
  

class Solution {
public:
    int numDistinct(string s, string t) {
        // 注意这里谁是行,谁是列,注意与dp表统一
        int m = t.size();
        int n = s.size();

        // 1、创建dp表
        vector<vector<double>> dp(m+1,vector<double>(n+1,0));
        // 2、初始化:将第一行初始化为1
        for(int j = 0; j < n+1; ++j) dp[0][j] = 1;
        // 3、填表:从上往下,从左往右
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                dp[i][j] += dp[i][j-1];
                if(s[j - 1] == t[i - 1])//注意原表和dp表中元素位置的映射关系
                    dp[i][j] += dp[i-1][j-1];
            }
        }
        // 4、返回
        return dp[m][n];
    }
};

  
  
  
  
  
  
  
  
  
  
  
  

8.4、通配符匹配(hard)

  题源:链接。

在这里插入图片描述

  
  

8.4.1、题解

  1)、思路分析
  对于两个数组/字符串之间的dp问题,一般的思考方式如下:
  1)、选取第一个字符串的[0,i]区间以及第二个字符串的[0,j]区间当成研究对象,结合题目的要求来定义状态表示;
  2)、然后根据两个区间上 “最后一个位置的字符”,来进行“分类讨论”,从而确定“状态转移方程”。
  我们可以根据上面的策略,解决大部分关于两个字符串之间的dp问题。
  

  1、确定状态表示: 根据上述,dp[i][j]表示,字符串p[0,j] 区间内的子串,能否完全匹配字符串s[0, 1]区间内的所有子串。
  
  

  2、推导状态转移方程: 要在p中找匹配s的字符,仍旧根据最后一个元素分情况讨论。根据题目,这里,p[j]可以有三类情况:
  
  1)、p[j] 为字母,这时候 p 要完全匹配 s ,则意味着 p[j] == s[i]。然后,我们需要看 p[j-1]区间内的子能否完全匹配上s[i-1]区间内的所有字符。即有:

	条件:p[j] = s[i] && dp[i-1][j-1] == true
	满足上述条件,则dp[i][j] = true,否则,dp[i][j] = false.

  
  2)、p[j] == '?',根据题意,'?' 只能匹配一个字符,此时,就需要看p[j-1]区间内的子串能否完全匹配上 s[i-1]区间内的所有字符。

	条件:p[j] == '?' && dp[i-1][j-1] == true
	满足上述条件,则dp[i][j] = true,否则,dp[i][j] = false.

在这里插入图片描述

  3)、p[j] == '*',根据题意,'*'能匹配任意多个字符。那么情况就有很多种:

'*'匹配0个字符时(空串),dp[i][j] 的状态,需要看 dp[i][j-1]

'*'匹配1个字符时,dp[i][j] 的状态,需要看 dp[i-1][j-1]
'*'匹配2个字符时,dp[i][j] 的状态,需要看 dp[i-2][j-1]
'*'匹配3个字符时,dp[i][j] 的状态,需要看 dp[i-3][j-1]
......
'*'匹配i个字符时,dp[i][j] 的状态,需要看 dp[0][j-1]

在上述这些情况中,只要有一种情况成立,那么dp[i][j] = true,即:
dp[i][j] = dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || dp[i-3][j-1] || ......

在这里插入图片描述

  那么问题来了,枚举ij位置,我们就需要两层for循环,这种情况又来一层小小的嵌套循环,三层循环下,时间复杂度会达到 O ( n 3 ) O(n^3) O(n3),有没有什么优化方法?(PS:解题时不优化,全情况或上,也是能通过的)
  
  以下举例优化理解:
  1、使用数学公式推导进行优化:
在这里插入图片描述

  可以发现,(2)式就是(1)式除第一项dp[i][j-1]以外的后面部分,代进去,即可得 d p [ i ] [ j ] = d p [ i ] [ j − 1 ] ∣ ∣ d p [ i − 1 ] [ j ] dp[i][j] = dp[i][j-1] || dp[i-1][j] dp[i][j]=dp[i][j1]∣∣dp[i1][j]

  
  2、根据状态表示以及实际情况优化:

当p[j] = '*'时,
	1)、p[j]匹配空串,则dp[i][j]的情况需要看s[0,i]和p[j-1],即dp[i][j-1]
	2)、p[j]匹配一个,但不舍去,此时dp[i][j]的情况需要看s[0,i-1]和p[j],即dp[i-1][j]

在这里插入图片描述

  如果不能理解,可以多翻找一下官方解答中的评论区大佬们的补充解释。
在这里插入图片描述

  
  
  
  3、初始化: 为了处理边界情况,引入虚拟节点进行初始化。需要注意虚拟节点中的填值,以及下标映射关系。
  1)、首行初始化(dp[0][j]):当输入字符串 s 为空时,字符模式 p 要能够匹配空字符串 s,则 p 必须仅由 '*' 字符组成(因为 '*' 可以匹配任意长度的字符序列,包括空序列)。因此,我们只用检查是否遇到了非 '*' 字符。如果遇到,则从该位置开始,dp[0][j] 为 false。
  2)、首列初始化(dp[i][0]):当字符模式 p 为空时,除非输入字符串 s 也为空(即 dp[0][0] 的情况),否则 p 无法匹配任何非空字符串 s。因此,对于所有 i > 0,dp[i][0] 应初始化为 false。
在这里插入图片描述

  
  
  4、填表顺序:根据状态转移方程,从上往下填每一行,每行从左到右。
  
  
  5、返回值:根据状态表示,因为引入了虚拟节点,需要返回dp[m][n]处的值。
  
  
  
  2)、题解
  

class Solution {
public:
    bool isMatch(string s, string p) {
        // 这里注意行、列分别是谁,和dp表中行、列,以及状态方程对应上
        int m = s.size();
        int n = p.size();

        // 1、建表并初始化:
        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
        
        s = " "+s;//引入虚拟节点,为方便映射关系,这里对原序列修改
        p = " "+p;

        dp[0][0] = true;
        for(int j = 1; j < n+1; ++j)//对首行初始化(首列除了[0][0]皆为false)
        {
            if(p[j] == '*') dp[0][j] = true;
            else break;//只要首次出现非'*',后续全不能匹配
        }

        // 2、填表:从上往下,从左往右
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                if(p[j] == '*')
                    dp[i][j] = dp[i][j-1] || dp[i-1][j];
                else
                    dp[i][j] = (p[j] == '?' || p[j] == s[i]) && dp[i-1][j-1];//将两种情况归为统一来写
            }
        }

        // 3、返回
        return dp[m][n];
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  

8.5、正则表达式匹配(hard)

  题源:链接。

在这里插入图片描述

  
  

8.5.1、题解

  1)、思路分析
  对于两个数组/字符串之间的dp问题,一般的思考方式如下:
  1)、选取第一个字符串的[0,i]区间以及第二个字符串的[0,j]区间当成研究对象,结合题目的要求来定义状态表示;
  2)、然后根据两个区间上 “最后一个位置的字符”,来进行“分类讨论”,从而确定“状态转移方程”。
  我们可以根据上面的策略,解决大部分关于两个字符串之间的dp问题。
  

  1、确定状态表示: 根据上述经验+题目描述,这里,dp[i][j]表示,字符串p[0,j]区间内的子串能否完全匹配上字符串s[0,i]区间内的子串。
  

  2、推导状态转移方程: 判断p是否能匹配上s,以p的最后一个元素p[j]分情况讨论:
  1)、p[j]a-z 的小写字母时,此时要使得 p 完全匹配上 s,则有s[i] == p[j],最后一个位置的字符匹配。然后,我们再看s[0,i-1]p[0,j-1]内的子串是否能完全匹配。既有:

	条件:p[j] = s[i] && dp[i-1][j-1] == true
	满足上述条件,则dp[i][j] = true,否则,dp[i][j] = false.

  
  2)、p[j] == '.',根据题意,'.' 只能匹配任意单个字符,此时无论s[i]处是什么字母,均能匹配。那么我们只需看p[j-1]区间内的子串能否完全匹配上 s[i-1]区间内的所有字符即可。既有:

	条件:p[j] == '.' && dp[i-1][j-1] == true
	满足上述条件,则dp[i][j] = true,否则,dp[i][j] = false.

  
  3)、p[j] == '*'时,要看前一个字符p[j-1] 。题目保证每次出现字符 '*' 时,前面都匹配到有效的字符。即:

a、p[j-1] =='.'时,即p[j-1]、p[j]'.*',此时:
	'.*'可以是0'.',那么此时dp[i][j]需要看p[0,j-2]能否匹配上s[0,i]子串,即dp[i][j-2]
	'.*'可以是1'.',那么此时dp[i][j]需要看p[0,j-2]能否匹配上s[0,i-1]子串,即dp[i-1][j-2]
	'.*'可以是2'.',那么此时dp[i][j]需要看p[0,j-2]能否匹配上s[0,i-2]子串,即dp[i-2][j-2]
	'.*'可以是3'.',那么此时dp[i][j]需要看p[0,j-2]能否匹配上s[0,i-3]子串,即dp[i-3][j-2]
以此类推:只要满足其中一种情况即可,则有:dp[i][j] = dp[i][j-2]|| dp[i-1][j-2]||dp[i-2][j-2]||dp[i-3][j-2]||......

  同8.4题一样,若不进行优化处理,时间复杂度会非常高。优化处理的方式有二(见上题):
  
  从数学公式推导的角度:
① d p [ i ] [ j ] = d p [ i ] [ j − 2 ] ∣ ∣ d p [ i − 1 ] [ j − 2 ] ∣ ∣ d p [ i − 2 ] [ j − 2 ] ∣ ∣ d p [ i − 3 ] [ j − 2 ] ∣ ∣ . . . . . . ① dp[i][j] = dp[i][j-2]|| dp[i-1][j-2]||dp[i-2][j-2]||dp[i-3][j-2]||...... dp[i][j]=dp[i][j2]∣∣dp[i1][j2]∣∣dp[i2][j2]∣∣dp[i3][j2]∣∣......
  设 i = i − 1 i = i-1 i=i1,此时有:
② d p [ i − 1 ] [ j ] = d p [ i − 1 ] [ j − 2 ] ∣ ∣ d p [ i − 2 ] [ j − 2 ] ∣ ∣ d p [ i − 3 ] [ j − 2 ] ∣ ∣ d p [ i − 4 ] [ j − 2 ] ∣ ∣ . . . . . . ② dp[i-1][j] = dp[i-1][j-2]|| dp[i-2][j-2]||dp[i-3][j-2]||dp[i-4][j-2]||...... dp[i1][j]=dp[i1][j2]∣∣dp[i2][j2]∣∣dp[i3][j2]∣∣dp[i4][j2]∣∣......
  将 ② 式代入 ① 式得:(匹配一个空串,或匹配一个,然后保留p[j-1]、p[j])
d p [ i ] [ j ] = d p [ i ] [ j − 2 ] ∣ ∣ d p [ i − 1 ] [ j ] dp[i][j] = dp[i][j-2]|| dp[i-1][j] dp[i][j]=dp[i][j2]∣∣dp[i1][j]

b、p[j-1] == a~z字母时,同理。
	匹配空串,则需要看p[0,j-2]能否匹配上s[0,i]子串,即dp[i][j-2]
	匹配一个,然后保留。此时要看s[i] == p[j-1],以及dp[i-1][j]

  
  
  综上,我们对状态转移方程进行整理:

1、当p[j]!= '*',( p[j]'.'或a~z时):
	dp[i][j] = (p[j] == '.' || p[j] == s[i]) && (dp[i-1][j-1] == true);
2、当p[j]== '*'时,
	dp[i][j] = dp[i][j-2] || ((p[j-1] == '*' || p[j-1] == s[i]) && dp[i-1][j] == true)

  
  
  3、初始化: 引入虚拟节点,防止填表时越界,需要注意两点。
  1)、下标映射关系。可以对原字符头插占位字符,使得下标映射一致。也可以什么都不做,在填表时注意隐射即可。
  2)、虚拟节点的初始化,要保证后续填表时状态转移方程正确。
在这里插入图片描述

  
  
  4、填表顺序: 根据状态转移方程,从上往下填每一行,每行中每个元素从左往右填表。
  
  
  5、返回值: 根据状态表示,因为引入了虚拟节点,需要返回dp[m][n]处的值。
  
  

  2)、题解
  

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size();// 行
        int n = p.size();// 列,用于开辟dp表,注意这里行列与s、p两表对应关系,注意与dp表对应

        // 1、创建dp表:引入虚拟节点,(m+1)×(n+1)
        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
        // 2、初始化
        s = " " + s;// 为了方便初始化和后续填表,统一下标映射关系
        p = " " + p;
        dp[0][0] = true;
        for(int j = 2; j < n+1; j+=2)// 初始化首行(s为空串时)
        {
            if(p[j] == '*') dp[0][j] = true;
            else break;//首次不满足,后续均不满足
        }

        // 3、填表:从上往下,从左到右
        for(int i = 1; i <=m; ++i)
        {
            for(int j = 1; j <=n; ++j)
            {
                if(p[j] == '*')
                    dp[i][j] = dp[i][j-2] || ((p[j-1] == '.' || p[j-1] == s[i]) && dp[i-1][j]);
                else 
                    dp[i][j] = (p[j] == '.' || p[j] == s[i]) && dp[i-1][j-1];
            }
        }

        // 4、返回
        return dp[m][n];
    }
};

  
  
  
  
  
  
  
  
  
  
  

8.6、 交错字符串(medium)

  题源:链接。

在这里插入图片描述

  
  

8.6.1、题解

  1)、思路分析
  分析此题,给定三个字符串 s1、s2 和 s3,我们需要验证 s3 是否可以由 s1 和 s2 交错组成。注意理解,这里“交错”指子串交错,其实根据示例1就能看出,并非指前一个元素来源自是s1 ,后一个元素就必须是 s2 。

  根据经验+题目要求,这里是否需要三个状态变量 i、j、k 分别表示 s1、s2、s3 中的一段区间呢?回答是不需要的,我们只需要两个状态变量 ij,分别表示 s1 和 s2 中的待研究区间。那么 s3 的长度和组成,可以由 s1 和 s2 的长度及组合方式直接决定。

  引入虚拟节点:①根据题目示例3可知,空串在本题中是有研究意义的。②此外,s3的末尾下标位置需要依赖于s1和s2的状态变量。基于这两点,引入虚拟节点会方便很多。
在这里插入图片描述

  
  1、确定状态表示: dp[i][j],表示s1[1,i]区间内的子串,以及s2[1,j]区间内的子串,交错拼接后是否能组成s3[1,i+j]区间内的子串。

  2、推导状态转移方程: 根据上述状态表示,要知道s3要构成交错字符串,则其每一个元素必须来源自s1或s2。因此,可以以s3的最后一个字符的来源,分情况讨论:
  1)、如果s1[i] == s3[i+j],说明s3的最后一个字符来源于s1,此时只需要看 s1[1,i-1]s2[1,j] ,是否能拼接成 s3[1, i+j-1]即可,若能拼接,则为true,否则,则为false。
  2)、如果s2[i] == s3[i+j],说明s3的最后一个字符来源于s2,此时只需要看 s1[1,i]s2[1,j-1] ,是否能拼接成 s3[1, i+j-1]即可,若能拼接,则为true,否则,则为false。
  3)、若s3的最后一个字符均不来源自s1、s2,此时无论如何拼接都是不成立的。

  综上:

dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j])
         || (s2[j] == s3[i + j] && dp[i][j - 1])

  只要满足其中一个即可。
在这里插入图片描述

  
  3、初始化: 引入了虚拟节点, 初始化时需要注意两点:
  ①下标隐射关系:在s1、s2、s3中头插了占位符进行解决。
  ②虚拟节点的初始化值要保证填表正确。
在这里插入图片描述

  
  4、填表顺序: 根据状态方程,从上往下填每一行,每一行从左往右。
  
  5、返回值: 根据状态表示,需要返回dp[m][n]处的值。
  

  2)、题解
  

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int m = s1.size();
        int n = s2.size();
        if(m + n != s3.size()) return false;// 注意这里长度要求
        s1 = " " + s1;
        s2 = " " + s2;
        s3 = " " + s3;

        // 1、创建dp表并初始化
        vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));

        // 2、初始化
        dp[0][0] = true;
        for(int j = 1; j < n+1; ++j)// 首行:s1为空串,看s2与s3
        {
            if(s2[j] == s3[j]) dp[0][j] = true;
            else break;
        }
        for(int i = 1; i < m+1; ++i)// 首列:s2为空串,看s1与s3
        {
            if(s1[i] == s3[i]) dp[i][0] = true;
            else break;
        }

        // 3、填表
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                if((s1[i] == s3[i+j] && dp[i-1][j])
                 || (s2[j] == s3[i+j] && dp[i][j-1]))
                dp[i][j] = true;
            }
        }

        // 4、返回
        return dp[m][n];
    }
};

  填表中,还可以简写为:

        // 3、填表
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                dp[i][j] = (s1[i] == s3[i+j] && dp[i-1][j])
                 || (s2[j] == s3[i+j] && dp[i][j-1])
            }
        }

  
  
  
  
  
  
  
  

8.7、两个字符串的最小ASCII删除和(medium)

  题源:链接。

在这里插入图片描述

  
  

8.7.1、题解

  1)、思路分析
  分析题目,本题可以直接根据题目定义状态表示。当然,我们也可以使用正难则反的思想,将本题题意拆解为之前做过的套路,即找ASCII码值最大的公共子序列,如此一来,可以化用8.1中的思想。
在这里插入图片描述

  
  1、确定状态表示: 这里,我们使用正难则反的策略。根据经验+题目要求,dp[i][j]表示,在s1的[0,i]区间和s2的[0,j]区间,找公共子序列,使得ASCII码和最大。
  

  2、推导状态转移方程: 根据最后⼀个位置的元素分情况讨论:
  1)、当s1[i]==s2[j]时, 这意味着s1[0,i]s2[0,j]区间内,最大公共序列的最后一个字符,一定会落在ij下标的元素上。我们只需要在s1 的[0,i-1]区间和 s2 的 [0,-1] 区间中,找到一个ASCII码最大的公共子序列,然后再加上当前 s1[i]的ASCII码即可(或 s2[j] ,因为它们是相等的)。

dp[i][j] = dp[i-1][j-1] + ASCII(s1[i])

  
  2)、当s1[i] != s2[j]时,:公共子序列的最大和会有三种可能:
  ①忽略s1[i],在s1的[0,i-1]区间和s2的[0,j]区间中找公共子序列。即dp[i-1][j]
  ②忽略s2[j],在s1的[0,i]区间和s2的[0,j-1]区间中找公共子序列。即dp[i][j-1]
  ③同时忽略s1[i]s2[j],在s1的[0,i-1]区间和s2的[0,j-1]区间中找公共子序列。即dp[i-1][j-1]
在这里插入图片描述

//实际上,第三种情况,就包含在一二情况中。但这里求max,列出三者进行比较并不影响结果。
dp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  
  
  3、初始化: 这里,空串具有研究意义的,因此我们引入虚拟节点,将原始dp表的规模多加上一行、一列,表示空串。此时需要注意两点:
  ①原s1、s2和dp表的下标映射关系。
  ②虚拟行、列中的填值,要保证后续填表结果是正确的。
在这里插入图片描述

  
  4、填表顺序: 根据状态表示,从上往下填写每一行,从左往右填写每一列。
在这里插入图片描述
  
  5、返回值: 因为这里使用了“正难则反”的策略,我们不能直接返回dp表里面的某个值。需要进行处理:

1)、先找到dp[m][n],也是最大公共ASCII 和;
2)、统计两个字符串的ASCII 码和sum;
3)、返回sum - 2*dp[m][n]

  
  
  2)、题解
  时空复杂度: O ( m ∗ n ) O(m*n) O(mn)

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int m = s1.size();
        int n = s2.size();
        // 1、创建dp表并初始化:注意这里行列关系,一一对应即可
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        // 2、填表:从左到右,从上到下
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j <n+1; ++j)
            {
                if(s1[i-1] == s2[j-1])//注意下标映射
                    dp[i][j] = dp[i-1][j-1] + s1[i-1];
                else
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
            }
        }
        // 3、返回
        int sum = 0;
        for(auto ch : s1) sum += ch;
        for(auto ch : s2) sum += ch;
        return sum - dp[m][n]*2;
    }
};

  其它写法:

        // 2、填表:从左到右,从上到下
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j <n+1; ++j)
            {
                dp[i][j] = max(dp[i-1][j],dp[i][j-1]);// 这两种情况一定存在
                if(s1[i-1] == s2[j-1])//注意下标映射
                    dp[i][j] = max(dp[i-1][j-1] + s1[i-1], dp[i][j]);
            }
        }

  
  
  
  
  
  
  
  
  
  
  
  

8.8、 最长重复子数组(medium)

  题源:链接。

在这里插入图片描述

  
  

8.8.1、题解

  1)、思路分析
  可以发现,这题和8.1题很像,但区别在于,本题要找的是最长公共子数组(子数组和子序列的区别在之前已经提及过)。
  
  1、确定状态表示: 首先,我们需要明确,如果将状态定义为数组nums1的[0, i]区间和数组nums2的[0, j]区间内的最长公共子数组的长度,这并不符合我们的要求。因为公共子数组必须是连续的,所以在考虑到达ij位置的最长公共子数组时,必须保证该子数组的前一个元素位于i-1j-1的位置
在这里插入图片描述

  因此,我们需要重新定义状态表示:

dp[i][j]:表示以nums1中第i个元素结尾,且以nums2中第j个元素结尾的最长公共子数组的长度。

  
  2、推导状态转移方程: 可以根据nums1[i]nums2[j]是否相等来分情况讨论:
  1)、当nums1[i] == nums2[j]时:这意味着nums1[i]nums2[j]可以加入到之前的最长公共子数组中,从而形成一个更长的公共子数组。因此,dp[i][j]的值应该是dp[i-1][j-1] + 1,即之前的最长公共子数组长度加1
  2)、当nums1[i] != nums2[j]时:此时,nums1[i]nums2[j]不能形成公共子数组的一部分。因此,dp[i][j]的值应该是0,表示在当前以ij位置为结尾的子数组中,没有公共子数组。
  
  
  3、初始化: 为了防止越界,可以引入虚拟节点,添加一行和一列。需要注意两个问题:
  ①下标映射关系。
  ②虚拟节点填值。这里,首行dp[0][j],表示nums1为空数组,首列dp[i][0],表示nums2为空数组。当nums1或nums2其中一个数组为空时,最长公共子数组长度为0
  
  
  4、填表顺序: 根据状态方程,从上往下填每一行,每一行左到右。

  5、返回值: 根据我们的状态表示,因该返回dp表中的最大值,它表示了nums1和nums2中公共的、长度最长的子数组的长度。
  
  

  2)、题解
  时空复杂度: O ( m ∗ n ) O(m*n) O(mn)

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();

        // 1、创建dp表并初始化
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));

        // 2、填表
        int ret = 0;
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(nums1[i-1] == nums2[j-1])// 注意下标映射
                    dp[i][j] = dp[i-1][j-1] + 1;
                ret = max(dp[i][j],ret);
            }
        }

        // 3、返回
        return ret;
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述


http://www.kler.cn/a/420761.html

相关文章:

  • Linux 各个目录作用
  • 第2章 汇编语言--- 计算机体系结构概览
  • 小红书矩阵运营:怎么通过多个账号来提升品牌曝光?
  • 【系统架构设计师】真题论文: 论软件质量保证及其应用(包括解题思路和素材)
  • ubuntu 和windows时区设置和时间修改
  • 初始化列表与Static成员
  • 基于Java Springboot成人教育APP且微信小程序
  • Web实时通信@microsoft/signalr
  • C语言第十四周课——课堂练习
  • pip更换国内源,加速Python包下载(附2024年12月最新国内镜像源列表)
  • Unity3D 设置图片拉伸四角不变形
  • PhPMyadmin-漏洞复现
  • 工业公辅车间数智化节能头部企业,蘑菇物联选择 TDengine 升级 AI 云智控
  • [在线实验]-RabbitMQ镜像的下载与部署
  • android 阻止返回退出
  • 【笔记总结】华为云:应用上云后的安全规划及设计
  • form表单阻止默认事件及获取值
  • PH热榜 | 2024-12-02
  • Milvus Cloud 2.5:向量数据库的新里程碑与全文检索的革新
  • 大数据治理:解锁数据价值,引领未来创新
  • windows C#-测试引用相等性
  • 人机交互中的状态交互、趋势交互
  • vue 3中使用复制功能
  • C++【PCL】利用矩阵对点云进行刚体变换
  • golang的wails框架在macos下的问题
  • 基于STM32的电能监控系统设计:ModBus协议、RS-485存储和分析电能数据(代码示例)