【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的情况也包含在内:其依赖的位置本身就初始化为了0elsedp[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;elsedp[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;elsedp[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;elsedp[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、初始化:将第一行初始化为1for(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];elsedp[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];elsedp[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.rhkb.cn/news/483131.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 3 教程第33篇(MySQL - mysql-connector 驱动)

Python MySQL - mysql-connector 驱动 MySQL 是最流行的关系型数据库管理系统&#xff0c;如果你不熟悉 MySQL&#xff0c;可以阅读我们的 MySQL 教程。 本章节我们为大家介绍使用 mysql-connector 来连接使用 MySQL&#xff0c; mysql-connector 是 MySQL 官方提供的驱动器。…

LLM*:路径规划的大型语言模型增强增量启发式搜索

路径规划是机器人技术和自主导航中的一个基本科学问题&#xff0c;需要从起点到目的地推导出有效的路线&#xff0c;同时避开障碍物。A* 及其变体等传统算法能够确保路径有效性&#xff0c;但随着状态空间的增长&#xff0c;计算和内存效率会严重降低。相反&#xff0c;大型语言…

【Db First】.NET开源 ORM 框架 SqlSugar 系列

.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…

企业品牌曝光的新策略:短视频矩阵系统

企业品牌曝光的新策略&#xff1a;短视频矩阵系统 在当今数字化时代&#xff0c;短视频已经渗透到我们的日常生活之中&#xff0c;成为连接品牌与消费者的关键渠道。然而&#xff0c;随着平台于7月20日全面下线了短视频矩阵的官方接口&#xff0c;许多依赖于此接口的小公司和内…

006 MATLAB编程基础

01 M文件 MATLAB输入命令有两种方法&#xff1a; 一是在MATLAB主窗口逐行输入命令&#xff0c;每个命令之间用分号或逗号分隔&#xff0c;每行可包含多个命令。 二是将命令组织成一个命令语句文集&#xff0c;使用扩展名“.m”&#xff0c;称为M文件。它由一系列的命令和语句…

Java基于SpringBoot+Vue的IT技术交流和分享平台(附源码+lw+部署)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【计算机网络】实验3:集线器和交换器的区别及交换器的自学习算法

实验 3&#xff1a;集线器和交换器的区别及交换器的自学习算法 一、 实验目的 加深对集线器和交换器的区别的理解。 了解交换器的自学习算法。 二、 实验环境 • Cisco Packet Tracer 模拟器 三、 实验内容 1、熟悉集线器和交换器的区别 (1) 第一步&#xff1a;构建网络…

UICollectionView在xcode16编译闪退问题

使用xcode15运行工程&#xff0c;控制台会出现如下提示&#xff1a; Expected dequeued view to be returned to the collection view in preparation for display. When the collection views data source is asked to provide a view for a given index path, ensure that a …

Proteus8.17下载安装教程

Proteus是一款嵌入式系统仿真开发软件&#xff0c;实现了从原理图设计、单片机编程、系统仿真到PCB设计&#xff0c;真正实现了从概念到产品的完整设计&#xff0c;其处理器模型支持8051、HC11、PIC10/12/16/18/24/30/DsPIC33、AVR、ARM、8086和MSP430等&#xff0c;能够帮助用…

Vue教程|搭建vue项目|Vue-CLI2.x 模板脚手架

一、项目构建环境准备 在构建Vue项目之前&#xff0c;需要搭建Node环境以及Vue-CLI脚手架&#xff0c;由于本篇文章为上一篇文章的补充&#xff0c;也是为了给大家分享更为完整的搭建vue项目方式&#xff0c;所以环境准备部分采用Vue教程&#xff5c;搭建vue项目&#xff5c;V…

一款支持80+语言,包括:拉丁文、中文、阿拉伯文、梵文等开源OCR库

大家好&#xff0c;今天给大家分享一个基于PyTorch的OCR库EasyOCR&#xff0c;它允许开发者通过简单的API调用来读取图片中的文本&#xff0c;无需复杂的模型训练过程。 项目介绍 EasyOCR 是一个基于Python的开源项目&#xff0c;它提供了一个简单易用的光学字符识别&#xff…

cocotb pytest

打印python中的print &#xff0c; 应该使用 pytest -s

【C++】STL——map和set

目录 1、序列式容器和关联式容器前 2、set 2.1 set类的介绍 2.2 set的构造和迭代器 2.3 set的增删查 set 的插入 set的查找 set的删除 2.4 multiset和set的差异 3、map 3 .1 pair类型 3.2 map的构造 3.3 map的增删查 map的构造遍历 map的插入 map的删除 map的查…

java基础概念46-数据结构1

一、引入 List集合的三种实现类使用了不同的数据结构&#xff01; 二、数据结构的定义 三、常见的数据结构 3-1、栈 特点&#xff1a;先进后出&#xff0c;后进先出。 java内存容器&#xff1a; 3-2、队列 特点&#xff1a;先进先出、后进后出。 栈VS队列-小结 3-3、数组 3-…

Docker:在 ubuntu 系统上生成和加载 Docker 镜像

本文将介绍在 ubuntu系统上进行 Docker 镜像的生成和加载方法和代码。 文章目录 一、下载和安装 docker二、加载 docker 文件三、保存你的镜像四、将镜像上传到云端并通过连接下载和加载 Docker 镜像五、Docker 容器和本地的文件交互5.1 从容器复制文件到本地宿主机5.1.1 单个文…

《数据挖掘:概念、模型、方法与算法(第三版)》

嘿&#xff0c;数据挖掘的小伙伴们&#xff01;今天我要给你们介绍一本超级实用的书——《数据挖掘&#xff1a;概念、模型、方法与算法》第三版。这本书是数据挖掘领域的经典之作&#xff0c;由该领域的知名专家编写&#xff0c;系统性地介绍了在高维数据空间中分析和提取大量…

做异端中的异端 -- Emacs裸奔之路4: 你不需要IDE

确切地说&#xff0c;你不需要在IDE里面编写或者阅读代码。 IDE用于Render资源文件比较合适&#xff0c;但处理文本&#xff0c;并不划算。 这的文本文件&#xff0c;包括源代码&#xff0c;配置文件&#xff0c;文档等非二进制文件。 先说说IDE带的便利: 函数或者变量的自动…

【C++】编程题目分析与实现回顾:从浮点数运算到整型转换的全面解读

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目一&#xff1a;计算成绩问题分析与优化实现优化后的实现优势 &#x1f4af;题目二&#xff1a;浮点数向零舍入不同实现方式的比较1. 使用强制类型转换 (int)2. 使用标准…

时间表格Java

输入&#xff1a;XXX XXX 小时 分钟 输出&#xff1a; XXX&#xff1a;XXX ~ XXX: XXX XXX&#xff1a;XXX ~ XXX: XXX XXX&#xff1a;XXX ~ XXX: XXX 处理&#xff1a;间隔五分钟、区间45分钟 14:15 ~ 15:0 15:5 ~ 15:50 15:55 ~ 16:40 16:45 ~ 17:30 17:35 ~ 18:20…

Spring AOP 的实现和切点表达式的介绍

1. 快速入手 AOP&#xff1a;就是面相切面编程&#xff0c;切面指的就是某一类特定的问题&#xff0c;也可以理解为面相特定方法编程&#xff0c;例如之前使用的拦截器&#xff0c;就是 AOP 思想的一种应用&#xff0c;统一数据返回格式和统一异常处理也是 AOP 思想的实现方式…