【算法专题】动态规划综合篇

news2024/11/17 10:53:56

动态规划7.0

    • 1. 最长公共子序列
    • 2. 不相交的线
    • 3. 不同的子序列
    • 4. 通配符匹配
    • 5. 正则表达式匹配
    • 6. 交错字符串
    • 7. 两个字符串的最小ASCII删除和
    • 8. 最长重复子数组

1. 最长公共子序列

题目链接 -> Leetcode -1143.最长公共子序列

Leetcode -1143.最长公共子序列

题目:给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成

思路:

  1. 状态表示:对于两个数组的动态规划,我们的定义状态表示的经验就是:选取第一个数组 [0, i] 区间以及第二个数组 [0, j] 区间作为研究对象;结合题目要求,定义状态表示。在本题中,我们根据定义状态表示为:
  • dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,最长公共子序列的长度;
  1. 状态转移方程:分析状态转移方程的经验就是根据「最后一个位置」的状况,分情况讨论。对于 dp[i][j] ,我们可以根据 s1[i] 与 s2[j] 的字符分情况讨论:
  • 两个字符相同, s1[i] = s2[j] :那么最长公共子序列就在 s1 的 [0, i - 1] 以及 s2 的 [0, j - 1] 区间上找到一个最长的,然后再加上 s1[i] 即可。因此 dp[i][j] = dp[i - 1][j - 1] + 1 ;
  • 两个字符不相同, s1[i] != s2[j] :那么最长公共子序列一定不会同时以 s1[i] 和 s2[j] 结尾。那么我们找最长公共子序列时,有下面三种策略:
    i. 去 s1 的 [0, i - 1] 以及 s2 的 [0, j] 区间内找:此时最大长度为 dp[i - 1][j] ;
    ii. 去 s1 的 [0, i] 以及 s2 的 [0, j - 1] 区间内找:此时最大长度为 dp[i][j - 1] ;
    iii. 去 s1 的 [0, i - 1] 以及 s2 的 [0, j - 1] 区间内找:此时最大长度为 dp[i - 1][j - 1] 。

我们要三者的最大值即可。但是我们细细观察会发现,第三种包含在第一种和第二种情况里面,但是我们求的是最大值,并不影响最终结果。因此只需求前两种情况下的最大值即可;

综上,状态转移方程为:

  • if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1 ;
  • if(s1[i] != s2[j]) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
  1. 返回值:根据「状态表示」得:返回 dp[m][n];

代码如下:

		class Solution {
		public:
		    int longestCommonSubsequence(string text1, string text2) 
		    {
		        int m = text1.size(), n = text2.size();
		
		        // dp[i][j] 表示 test1中 0~i 和 test2中 0~j 字符串中最长公共子序列的长度  
		        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
		
		        // 因为dp多开了一个空间,所以使字符串统一向后移动一个单位,使下标一一对应
		        text1 = ' ' + text1, text2 = ' ' + text2;
		
		        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;
		
		                // 否则取test1中0~i-1 的字符串和test2中 0~j 的字符串 和 test1中 0~i 的字符串和test2中 0~j-1 的字符串的dp表中的较大值
		                else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		            }
		        }
		
		        return dp[m][n];
		    }
		};

2. 不相交的线

题目链接 -> Leetcode -1035.不相交的线

Leetcode -1035.不相交的线

题目:在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:

nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:
输入:nums1 = [1, 4, 2], nums2 = [1, 2, 4]
输出:2
在这里插入图片描述

解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1] = 4 到 nums2[2] = 4 的直线将与从 nums1[2] = 2 到 nums2[1] = 2 的直线相交。

示例 2:
输入:nums1 = [2, 5, 1, 2, 5], nums2 = [10, 5, 2, 1, 5, 2]
输出:3

示例 3:
输入:nums1 = [1, 3, 7, 1, 7, 5], nums2 = [1, 9, 2, 5, 1]
输出:2

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • 1 <= nums1[i], nums2[j] <= 2000

思路:如果要保证两条直线不相交,那么我们「下一个连线」必须在「上一个连线」对应的两个元素的「后面」寻找相同的元素。这就转化成「最长公共子序列」的模型了。那就是在这两个数组中寻找「最长的公共子序列」。

代码如下:

		class Solution {
		public:
		    // 与最长公共子序列同类型
		    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) 
		    {
		        int m = nums1.size(), n = nums2.size();
		        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
		
		        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]);
		            }
		        }
		
		        return dp[m][n];
		    }
		};

3. 不同的子序列

题目链接 -> Leetcode -115.不同的子序列

Leetcode -115.不同的子序列

题目:给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 10^9 + 7 取模。

示例 1:
输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。
rabbbit
rabbbit
rabbbit

示例 2:
输入:s = “babgbag”, t = “bag”
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 “bag” 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag

提示:

  • 1 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成

思路:

  1. 状态表示:对于两个字符串之间的 dp 问题,我们一般的思考方式如下:
  • 选取第一个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义「状态表示」;
  • 然后根据两个区间上「最后一个位置的字符」,来进行「分类讨论」,从而确定「状态转移方程」。

我们可以根据上面的策略,解决大部分关于两个字符串之间的 dp 问题。

  • dp[i][j] 表示:在字符串 s 的 [0, j] 区间内的所有子序列中,有多少个 t 字符串 [0, i] 区间内的子串;
  1. 状态转移方程:根据「最后一个位置」的元素,结合题目要求,分情况讨论:
  • 当 t[i] == s[j] 的时候,此时的子序列有两种选择:
    i. 一种选择是:子序列选择 s[j] 作为结尾,此时相当于在状态 dp[i - 1][j - 1]
    中的所有符合要求的子序列的后面,再加上一个字符 s[j] ,此时 dp[i][j] = dp[i - 1][j - 1] ;
    ii. 另一种选择是:不选择 s[j] 作为结尾;此时相当于选择了状态 dp[i][j - 1] 中所有符合要求的子序列。我们也可以理解为继承了上个状态里面的求得的子序列。此时 dp[i][j] = dp[i][j - 1] ;

两种情况加起来,就是 t[i] == s[j] 时的结果。

  • 当 t[i] != s[j] 的时候,此时的子序列只能从 dp[i][j - 1] 中选择所有符合要求的子序列。只能继承上个状态里面求得的子序列,dp[i][j] = dp[i][j - 1] ;

综上所述,状态转移方程为:

  • 所有情况下都可以继承上一次的结果: dp[i][j] = dp[i][j - 1] ;
  • 当 t[i] == s[j] 时,可以多选择一种情况: dp[i][j] += dp[i - 1][j - 1]
  1. 返回值:根据「状态表示」,返回 dp[m][n] 的值;

代码如下:

		class Solution {
		public:
		    int numDistinct(string s, string t)
		    {
		        int m = t.size(), n = s.size();
		        // dp[i][j] 表示,s字符串[0,j]区间内所有的子序列中,有多少个t字符串[0,i]区间内的子串
		        // 多开一行一列,引入空串
		        vector<vector<double>> dp(m + 1, vector<double>(n + 1));
		
		        // 当 s 为空串时,t 中怎么也会有一个空串,所以将第一行全部初始化为1
		        for (int j = 0; j <= n; j++) dp[0][j] = 1;
		
		        s = " " + s, t = " " + t; // 使下标一一对应
		
		        for (int i = 1; i <= m; i++)
		        {
		            for (int j = 1; j <= n; j++)
		            {
		                // 不算当前 s[j] ,统计s中 0~j-1 组成的字符串中有多少能组成 t 中0~i的子序列
		                dp[i][j] += dp[i][j - 1];
		                
		                // 算当前 s[j],当 s[j] == t[i] 时再算上前面的子序列累加起来
		                if (s[j] == t[i]) dp[i][j] += dp[i - 1][j - 1];
		            }
		        }
		        return dp[m][n];
		    }
		};

4. 通配符匹配

题目链接 -> Leetcode -44.通配符匹配

Leetcode -44.通配符匹配

题目:给你一个输入字符串(s) 和一个字符模式( p),请你实现一个支持 ‘?’ 和 ’ * ’ 匹配规则的通配符匹配:
‘?’ 可以匹配任何单个字符。
’ * ’ 可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。

示例 1:
输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。

示例 2:
输入:s = “aa”, p = “*”
输出:true
解释:’ * ’ 可以匹配任意字符串。

示例 3:
输入:s = “cb”, p = “?a”
输出:false
解释:‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。

提示:

  • 0 <= s.length, p.length <= 2000
  • s 仅由小写英文字母组成
  • p 仅由小写英文字母、‘?’ 或 ‘*’ 组成

思路:

  1. 状态表示:对于两个字符串之间的 dp 问题,我们一般的思考方式如下:
  • 选取第一个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义「状态表示」;
  • 然后根据两个区间上「最后一个位置的字符」,来进行「分类讨论」,从而确定「状态转移方程」。

因此,我们定义状态表示为:dp[i][j] 表示: p 字符串 [0, j] 区间内的子串能否匹配字符串 s 的 [0, i] 区间内的子串。

  1. 状态转移方程:
    我们根据最后一个位置的元素,结合题目要求,分情况讨论:
  • 当 s[i] == p[j] 或 p[j] == ‘?’ 的时候,此时两个字符串匹配上了当前的一个字符,只能从 dp[i - 1][j - 1] 中看当前字符前面的两个子串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i][j - 1] ;
  • 当 p[j] == ‘*’ 的时候,此时匹配策略有两种选择:
    i. 一种选择是: * 匹配空字符串,此时相当于它什么都没有匹配,直接继承状态 dp[i][j - 1] ,此时 dp[i][j] = dp[i][j - 1] ;
    ii. 另⼀种选择是: * 向前匹配 1 ~ n 个字符,直至匹配上整个 s1 串。此时相当于从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的情况。此时 dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;
  • 当 p[j] 不是特殊字符,且不与 s[i] 相等时,无法匹配。

三种情况加起来,就是所有可能的匹配结果。

即下图分析:

在这里插入图片描述
综上所述,状态转移方程为:

  • 当 s[i] == p[j] 或 p[j] == ‘?’ 时: dp[i][j] = dp[i][j - 1] ;
  • 当 p[j] == ‘*’ 时,有多种情况需要讨论: dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;

优化:当我们发现,计算一个状态的时候,需要一个循环才能搞定的时候,我们要想到去优化。优化的方向就是用一个或者两个状态来表示这一堆的状态。通常就是把它写下来,然后用数学的方式做一下等价替换:

当 p[j] == ‘*’ 时,状态转移方程为:
dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1] || …

我们发现 i 是有规律的减小的,因此我们去看看 dp[i - 1][j] :
dp[i - 1][j] = dp[i - 1][j - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1] …

我们发现, dp[i][j] 的状态转移方程里面除了第一项以外,其余的都可以用 dp[i - 1][j] 替代。因此,我们优化我们的状态转移方程为: dp[i][j] = dp[i - 1][j] || dp[i][j - 1].

  1. 初始化:

由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为 false;由于需要用到前一行和前一列的状态,我们初始化第一行、第一列即可;

  • dp[0][0] 表示两个空串能否匹配,答案是显然的, 初始化为 true.
  • 第一行表示 s 是一个空串, p 串和空串只有一种匹配可能,即 p 串表示为 *** ,此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 “*” 的 p 子串和空串的 dp 值设为 true.
  • 第一列表示 p 是一个空串,不可能匹配上 s 串,跟随数组初始化即可.
  1. 返回值:根据状态表示,返回 dp[m][n] 的值.

代码如下:

		class Solution {
		public:
		    bool isMatch(string s, string p) 
		    {
		        int m = s.size(), n = p.size();
		        s = ' ' + s, p = ' ' + p;
		
		        // dp[i][j] 表示: p 字符串 [0, j] 区间内的⼦串能否匹配字符串 s 的 [0, i] 区间内的⼦串。
		        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
		
		        // 完成初始化
		        // 多开一行一列,第一行相当于s为空串,第一列相当于p为空串
		        dp[0][0] = true; // 大家都是空串,可以匹配
		        for(int i = 1; i <= n; i++) 
		            if(p[i] == '*') dp[0][i] = true; // 在第一行中初始化中,只要 p 中出现不是'*'就不匹配
		            else break;
		
		        for(int i = 1; i <= m; i++)
		        {
		            for(int j = 1; j <= n; j++)
		            {
		                // 状态转移方程,分析 dp[i][j]等于什么,分情况讨论 
		                if(p[j] == '?' && dp[i - 1][j - 1]) dp[i][j] = true;  // 当 p[j] 是 '?' 时
		                else if(p[j] == '*') dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; // 当p[j]是 '*' 时,用数学推算出来
		                else if(p[j] == s[i] && dp[i - 1][j - 1]) dp[i][j] = true;  // 当p[j] 等于 s[i] 时
		            }
		        }
		
		        return dp[m][n];
		    }
		};

5. 正则表达式匹配

题目链接 -> Leetcode -10.正则表达式匹配

Leetcode -10.正则表达式匹配

题目:给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:
输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。

示例 2:
输入:s = “aa”, p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

示例 3:
输入:s = “ab”, p = “.*”
输出:true
解释:". * " 表示可匹配零个或多个(’ * ‘)任意字符(’.')。

提示:

  • 1 <= s.length <= 20
  • 1 <= p.length <= 20
  • s 只包含从 a - z 的小写字母。
  • p 只包含从 a - z 的小写字母,以及字符.和 * 。
  • 保证每次出现字符 * 时,前面都匹配到有效的字符

思路:

  1. 状态表示:dp[i][j] 表示:字符串 p 的 [0, j] 区间和字符串 s 的 [0, i] 区间是否可以匹配;
  2. 状态转移方程:根据最后⼀个位置的元素,结合题⽬要求,分情况讨论:
  • 当 s[i] == p[j] 或 p[j] == ‘.’ 的时候,此时两个字符串匹配上了当前的⼀个字符,只能从 dp[i - 1][j - 1] 中看当前字符前⾯的两个⼦串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i - 1][j - 1] ;
  • 当 p[j] == '’ 的时候,和上道题稍有不同的是,上道题 "" 本⾝便可匹配 0 ~ n 个字符,但此题是要带着 p[j - 1] 的字符⼀起,匹配 0 ~ n 个和 p[j - 1] 相同的字符。此时,匹配策略有两种选择:
    a. ⼀种选择是: p[j - 1]* 匹配空字符串,此时相当于这两个字符都没有撇配,直接继承状态 dp[i][j - 2] ,此时 dp[i][j] = dp[i][j - 2] ;
    b. 另⼀种选择是: p[j - 1]* 向前匹配 1 ~ n 个字符,直⾄匹配上整个 s1 串;此时相当于从 dp[k][j - 2] (0 < k <= i) 中所有匹配情况中,选择性继承可以成功的情况。此时 dp[i][j] = dp[k][j - 2] (0 < k <= i 且 s[k]~s[i] = p[j - 1]) ;
  • 当 p[j] 不是特殊字符,且不与 s[i] 相等时,无法匹配。

三种情况加起来,就是所有可能的匹配结果。

在这里插入图片描述

综上所述,状态转移方程为:

  • 当 s[i] == p[j] 或 p[j] == ‘.’ 时: dp[i][j] = dp[i][j - 1] ;
  • 当 p[j] == ‘*’ 时,有多种情况需要讨论: dp[i][j] = dp[i][j - 2] ;dp[i][j] = dp[k][j - 1] (0 <= k <= i)

优化:当我们发现,计算一个状态的时候,需要一个循环才能搞定的时候,我们要想到去优化。优化的方向就是用一个或者两个状态来表示这一堆的状态。通常就是把它写下来,然后用数学的方式做一下等价替换:

当 p[j] == ‘*’ 时,状态转移方程为:
dp[i][j] = dp[i][j - 2] || dp[i - 1][j - 2] || dp[i - 2][j - 2] …

我们发现 i 是有规律的减小的,因此我们去看看 dp[i - 1][j] :
dp[i - 1][j] = dp[i - 1][j - 2] || dp[i - 2][j - 2] || dp[i - 3][j - 2] …

我们发现, dp[i][j] 的状态转移方程里面除了第一项以外,其余的都可以用 dp[i - 1][j] 替代。因此,我们优化我们的状态转移方程为: dp[i][j] = dp[i][j - 2] || dp[i - 1][j] .

  1. 初始化:

由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为 false.

由于需要用到前一行和前一列的状态,我们初始化第一行、第一列即可;dp[0][0] 表示两个空串能否匹配,答案是显然的, 初始化为 true;第一行表示 s 是一个空串, p 串和空串只有一种匹配可能,即 p 串全部字符表示为 “任⼀字符+ *”,此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "任⼀字符 + *"的 p 子串和空串的 dp 值设为 true.

  1. 返回值:根据状态表示,返回 dp[m][n] 的值.

代码如下:

		class Solution {
		public:
		    bool isMatch(string s, string p) 
		    {
		        int m = s.size(), n = p.size();
		
		        // dp[i][j] 表⽰,字符串 p 的 [0, j] 区间和字符串 s 的 [0, i] 区间是否可以匹配
		        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
		        s = ' ' + s, p = ' ' + p;
		
		        // 完成初始化
		        // 第⼀⾏表⽰ s 是⼀个空串, p 串和空串只有⼀种匹配可能,即 p 串全部字符表⽰为 "任⼀字符+ *",此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "任⼀字符 + *"的 p ⼦串和空串的 dp 值设为 true 。因为 "任⼀字符+ *" 可以表示空串
		        dp[0][0] = true;
		        for(int i = 2; i <= n; i += 2) 
		            if(p[i] == '*') dp[0][i] = true, dp[0][i - 1] = true;
		            else break;
		
		        for(int i = 1; i <= m; i++)
		        {
		            for(int j = 1; j <= n; j++)
		            {
		                // dp[i][j] = dp[i][j - 2] || dp[i - 1][j] 使用数学推导
		                // dp[i][j - 2] 不会报错是因为第一个位置不可能是'*'
		                if(p[j] == '*') 
		                    dp[i][j] = dp[i][j - 2] || dp[i - 1][j] && (p[j - 1] == s[i] || p[j - 1] == '.');
		                else 
		                    dp[i][j] = ((p[j] == s[i] || p[j] == '.') && dp[i - 1][j - 1]);
		            }
		        }
		        return dp[m][n];
		    }
		};

6. 交错字符串

题目链接 -> Leetcode -97.交错字符串

Leetcode -97.交错字符串

题目:给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。

两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

s = s1 + s2 + … + sn
t = t1 + t2 + … + tm
| n - m| <= 1
交错 是 s1 + t1 + s2 + t2 + s3 + t3 + … 或者 t1 + s1 + t2 + s2 + t3 + s3 + …
注意:a + b 意味着字符串 a 和 b 连接。

示例 1:
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true

示例 2:
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出:false

示例 3:
输入:s1 = “”, s2 = “”, s3 = “”
输出:true

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1、s2、和 s3 都由小写英文字母组成

思路:

  1. 状态表示:dp[i][j] 表示字符串 s1 中 [1, i] 区间内的字符串以及 s2 中 [1, j] 区间内的字符串,能否拼接成 s3 中 [1, i + j] 区间内的字符串;
  2. 状态转移方程:

先分析一下题目,题目中交错后的字符串为 s1 + t1 + s2 + t2 + s3 + t3… ,看似一个 s 一个 t 。实际上 s1 能够拆分成更小的一个字符,进而可以细化成 s1 + s2 + s3 + t1 + t2 + s4… ;也就是说,并不是前一个用了 s 的子串,后一个必须要用 t 的子串。这一点理解,对我们的状态转移很重要。

继续根据两个区间上「最后一个位置的字符」,结合题目的要求,来进行「分类讨论」:

  • 当 s3[i + j] = s1[i] 的时候,说明交错后的字符串的最后一个字符和 s1 的最后一个字符匹配了。那么整个字符串能否交错组成,变成:s1 中 [1, i - 1] 区间上的字符串以及 s2 中 [1, j] 区间上的字符串,能够交错形成 s3 中 [1, i + j - 1] 区间上的字符串,也就是 dp[i - 1][j] ;此时 dp[i][j] = dp[i - 1][j];
  • 当 s3[i + j] = s2[j] 的时候,说明交错后的字符串的最后一个字符和 s2 的最后一个字符匹配了。那么整个字符串能否交错组成,变成:s1 中 [1, i] 区间上的字符串以及 s2 中 [1, j - 1] 区间上的字符串,能够交错形成 s3 中 [1, i + j - 1] 区间上的字符串,也就是 dp[i][j - 1] ;
  • 当两者的末尾都不等于 s3 最后一个位置的字符时,说明不可能是两者的交错字符串。

上述三种情况下,只要有一个情况下能够交错组成目标串,就可以返回 true;因此,我们可以定义状态转移为:

  • dp[i][j] = (s1[i - 1] == s3[i + j - 1] && dp[i - 1][j]) || (s2[j - 1] == s3[i + j - 1] && dp[i][j - 1])
  1. 初始化:
    由于用到 i - 1 , j - 1 位置的值,因此需要初始化「第一个位置」以及「第一行」和「第一列」。
  • 第一个位置:dp[0][0] = true ,因为空串 + 空串能够构成一个空串。
  • 第一行:第一行表示 s1 是一个空串,我们只用考虑 s2 即可。因此状态转移之和 s2 有关:dp[0][j] = s2[j - 1] == s3[j - 1] && dp[0][j - 1] , j 从 1 到 n( n 为 s2 的长度)
  • 第一列:第一列表示 s2 是一个空串,我们只用考虑 s1 即可。因此状态转移之和 s1 有关:dp[i][0] = s1[i - 1] == s3[i - 1] && dp[i - 1][0] , i 从 1 到 m( m 为 s1 的长度)
  1. 返回值:根据「状态表示」,我们需要返回 dp[m][n] 的值。

代码如下:

		class Solution {
		public:
		    bool isInterleave(string s1, string s2, string s3) 
		    {
		        int l1 = s1.size(), l2 = s2.size(), l3 = s3.size();
		        if(l1 + l2 != l3) return false;
		
		        s1 = " " + s1, s2 = " " + s2, s3 = " " + s3;
		
		        // 完成初始化
		        // dp[i][j] 表⽰字符串 s1 中 [1, i] 区间内的字符串以及 s2 中 [1, j] 区间内的字符串,能否拼接成 s3 中 [1, i + j] 区间内的字符串
		        vector<vector<bool>> dp(l1 + 1, vector<bool>(l2 + 1));
		        dp[0][0] = true;
		
		        // 第⼀⾏表⽰ s1 是⼀个空串,我们只⽤考虑 s2 即可
		        for(int j = 1; j <= l2; j++) 
		            if(s2[j] == s3[j]) dp[0][j] = true;
		            else break;
		
		        // 第⼀列表⽰ s2 是⼀个空串,我们只⽤考虑 s1 即可
		        for(int i = 1; i <= l1; i++) 
		            if(s1[i] == s3[i]) dp[i][0] = true;
		            else break;
		
		        // 开始填表
		        for(int i = 1; i <= l1; i++)
		        {
		            for(int j = 1; j <= l2; j++)
		            {
		                // 比较 s1 和 s2 中最后一个字符和 s3 中最后一个字符是否相同
		                 dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j]) ||
		                             (s2[j] == s3[i + j] && dp[i][j - 1]);
		            }
		        }
		        return dp[l1][l2];
		    } 
		};

7. 两个字符串的最小ASCII删除和

题目链接 -> Leetcode -712.两个字符串的最小ASCII删除和

Leetcode -712.两个字符串的最小ASCII删除和

题目:给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。

示例 1:
输入: s1 = “sea”, s2 = “eat”
输出 : 231
解释 : 在 “sea” 中删除 “s” 并将 “s” 的值(115)加入总和。
在 “eat” 中删除 “t” 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。

示例 2 :
输入 : s1 = “delete”, s2 = “leet”
输出 : 403
解释 : 在 “delete” 中删除 “dee” 字符串变成 “let”,
将 100[d] + 101[e] + 101[e] 加入总和。在 “leet” 中删除 “e” 将 101[e] 加入总和。
结束时,两个字符串都等于 “let”,结果即为 100 + 101 + 101 + 101 = 403 。
如果改为将两个字符串转换为 “lee” 或 “eet”,我们会得到 433 或 417 的结果,比答案更大。

提示 :

  • 0 <= s1.length, s2.length <= 1000
  • s1 和 s2 由小写英文字母组成

思路:正难则反,求两个字符串的最小 ASCII 删除和,其实就是找到两个字符串中所有的公共子序列里面, ASCII 最大和。因此,我们的思路就是按照「最长公共子序列」的分析方式来分析。

  1. 状态表示:dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,公共子序列的 ASCII 最大和。
  2. 状态转移方程:

对于 dp[i][j] 根据「最后一个位置」的元素,结合题目要求,分情况讨论:

  • 当 s1[i] == s2[j] 时:应该先在 s1 的 [0, i - 1] 区间以及 s2 的 [0, j - 1] 区间内找一个公共子序列的最大和,然后在它们后面加上一个 s1[i] 字符即可。
    此时 dp[i][j] = dp[i - 1][j - 1] + s1[i] ;
  • 当 s1[i] != s2[j] 时:公共子序列的最大和会有三种可能:
    (1) s1 的 [0, i - 1] 区间以及 s2 的 [0, j] 区间内:此时 dp[i][j] = dp[i - 1][j] ;
    (2) s1 的 [0, i] 区间以及 s2 的 [0, j - 1] 区间内:此时 dp[i][j] = dp[i][j - 1] ;
    (3) s1 的 [0, i - 1] 区间以及 s2 的 [0, j - 1] 区间内:此时 dp[i][j] = dp[i - 1][j - 1] ;
    但是前两种情况里面包含了第三种情况,因此仅需考虑前两种情况下的最大值即可。

综上所述,状态转移方程为:

  • 当 s1[i - 1] == s2[j - 1] 时, dp[i][j] = dp[i - 1][j - 1] + s1[i] ;
  • 当 s1[i - 1] != s2[j - 1] 时, dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
  1. 返回值:根据「状态表示」,我们不能直接返回 dp 表里面的某个值:
    i. 先找到 dp[m][n] ,也是最大公共 ASCII 和;
    ii. 统计两个字符串的 ASCII 码和 s u m;
    iii. 返回 sum - 2 * dp[m][n]

代码如下:

		class Solution {
		public:
		    int minimumDeleteSum(string s1, string s2) 
		    {
		        int m = s1.size(), n = s2.size();   
		        //dp[i][j] 表⽰: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的⼦序列中,公共⼦序列的 ASCII 最⼤和。
		        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
		        s1 = ' ' + s1, s2 = ' ' + s2;
		        int sum = 0, flag = 1;
		
		        for(int i = 1; i <= m; i++)
		        {
		            for(int j = 1; j <= n; j++)
		            {
		                // 有s1[i]无s2[j] 或 无s1[i]有s2[j] (这两种情况包括无s1[i]无s2[j])
		                dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
		
		                // 既有s1[i]也有s2[j]
		                if(s1[i] == s2[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + s1[i]);
		
		                if(flag) sum += s2[j];  // 计算 s2 字符串总和,不能在循环外计算,会把前面的 ' ' 加上
		            }
		            sum += s1[i];  // 计算 s1 字符串总和
		            flag = 0;
		        }
		
		        return sum - 2*dp[m][n];
		    }
		};

8. 最长重复子数组

题目链接 -> Leetcode -718.最长重复子数组

Leetcode -718.最长重复子数组

题目:给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。

示例 1:
输入:nums1 = [1, 2, 3, 2, 1], nums2 = [3, 2, 1, 4, 7]
输出:3
解释:长度最长的公共子数组是[3, 2, 1] 。

示例 2:
输入:nums1 = [0, 0, 0, 0, 0], nums2 = [0, 0, 0, 0, 0]
输出:5

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 100

思路:子数组是数组中「连续」的一段,我们习惯上「以某一个位置为结尾」来研究。由于是两个数组,因此我们可以尝试:以第一个数组的 i 位置为结尾以及第二个数组的 j 位置为结尾来解决问题。

  1. 状态表示:dp[i][j] 表示「以第一个数组的 i 位置为结尾」,以及「第二个数组的 j 位置为结尾」公共的 、长度最长的「子数组」的长度。
  2. 状态转移方程:对于 dp[i][j] ,当 nums1[i] == nums2[j] 的时候,才有意义,此时最长重复子数组的长度应该等于 1 加上除去最后一个位置时,以 i - 1, j - 1 为结尾的最长重复子数组的长度。因此,状态转移方程为: dp[i][j] = 1 + dp[i - 1][j - 1]
  3. 返回值:根据「状态表示」,我们需要返回 dp 表里面的「最大值」

代码如下:

		class Solution {
		public:
		    int findLength(vector<int>& nums1, vector<int>& nums2) 
		    {
		        int m = nums1.size(), n = nums2.size();
		
		        //dp[i][j] 表⽰「以第⼀个数组的 i 位置为结尾」,以及「第⼆个数组的 j 位置为结尾」公共的 、⻓度最⻓的「⼦数组」的⻓度
		        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
		        int ret = INT_MIN;
		
		        for(int i = 1; i <= m; i++)
		        {
		            for(int j = 1; j <= n; j++)
		            {
		                // 两个元素相等
		                if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
		
		                ret = max(ret, dp[i][j]);
		            }
		        }
		
		        return ret;
		    }
		};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1418104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统论述题+第5、6、7、8、9章的知识小点总结(尤其是选择题)

文章目录 一、操作系统论述题怎么提高内存利用率&#xff1f;怎么提高CPU利用率&#xff1f;怎么提高操作系统并发度&#xff1f;这个答案也不知道是什么问题里面的 二、操作系统5、6、7、8、9章选择题知识点第五章&#xff1a;存储器管理第六章&#xff1a;虚拟存储器第七章&a…

【笔试常见编程题03】统计回文、连续最大和、不要二、把字符串转换成整数

1. 统计回文 “回文串”是一个正读和反读都一样的字符串&#xff0c;比如“level”或者“noon”等等就是回文串。花花非常喜欢这种拥有对称美的回文串&#xff0c;生日的时候她得到两个礼物分别是字符串A和字符串B。现在她非常好奇有没有办法将字符串B插入字符串A使产生的字符串…

如何通俗解释Docker是什么?

要想弄懂Docker&#xff0c;咱们得先从“容器化”讲起。 一、容器化技术及Docker的出现 容器化&#xff0c;它是一种轻量级、可移植的软件打包方式&#xff0c;你就想象成一个快递箱子&#xff0c;里面装着你的应用和所有需要运行的环境&#xff0c;这个箱子能在任何支持容器…

promethues基础概念

promethues是一个开源的系统监控以及报警系统&#xff0c;整个zabbix的功能&#xff0c;系统&#xff0c;网络&#xff0c;设备 promethues可以兼容网络和设置被&#xff0c;容器监控&#xff0c;告警系统&#xff0c;因为他和k8s是一个项目基金开发的产品&#xff0c;天生匹配…

【漏洞复现】中移铁通禹路由器信息泄露漏洞

Nx01 产品简介 中移禹路由器支持宽带拨号、动态IP和静态IP三种上网模式,一般中国移动宽带的光猫都是智能光猫也就是光猫带路由器功能,中移禹路由器作为二级路由使用。 Nx02 漏洞描述 中移禹路由器ExportSettings处存在信息泄露漏洞&#xff0c;攻击者可以获取后台权限。 Nx03…

【机器学习】欠拟合与过拟合

过拟合&#xff1a;模型在训练数据上表现良好对不可见数据的泛化能力差。 欠拟合&#xff1a;模型在训练数据和不可见数据上泛化能力都很差。 欠拟合常见解决办法&#xff1a; &#xff08;1&#xff09;增加新特征&#xff0c;可以考虑加入特征组合、高次特征&#xff0c;以…

【Mac】windows PC用户转用Mac 配置笔记

win转mac使用的一些配置笔记&#xff1b;感觉mac在UI上还是略胜一筹&#xff0c;再配合在win上的操作习惯就体验更好了&#xff0c;对日常办公需求的本人足以。 优化设置 主要 操作优化 AltTab&#xff1a; win 习惯查看全部活动的alt键&#xff0c;对比cmdtab多了可以预览&…

【qt】switchBtn

方法1 在qtdesigner中设置按钮图标的三个属性&#xff0c;normal off 、normal on和checkabletrue。 from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5 import uic from switchBtn import Ui_Dialogclass Test(QDialog, Ui_…

手敲Mybatis(17章)-二级缓存功能,感受装饰器的魅力

1.目的 本节主要是讲Mybatis的二级缓存&#xff0c;一级缓存是会话SqlSession级别的&#xff0c;二级缓存是Mapper级别的这个大家都知道&#xff0c;一级缓存主要是同一个SqlSession实例才可以缓存&#xff0c;那么出现commit等其他情况可能清除缓存&#xff0c;我想要再发起的…

ABB机器人单周和连续运行模式切换的配置方法

ABB机器人单周和连续运行模式切换的配置方法 有朋友反映:示教器上已经选择了“连续”模式,在通过PLC远程控制ABB机器人启动时,机器人的运行模式会从“连续”自动切换到“单周”, 那么哪里可以设置该选项呢,大家可以参考以下内容: 用户可以在快速设置栏设置机器人运行的运…

Q-Bench:一种用于低级别视觉通用基础模型的基准测试

1. 引言 多模态大语言模型&#xff08;Multi-modality Large Language Models&#xff0c;后续简称多模态大模型&#xff09;能够提供强大的通用级别视觉感知/理解能力&#xff0c;甚至可以通过自然语言与人类进行无缝对话和互动。虽然多模态大模型的这些能力已经在多个视觉语…

解析PreMaint在石油化工设备预测性维护领域的卓越表现

石油化工行业一直在寻找能够确保设备高效运行的先进维护解决方案。在这个领域&#xff0c;PreMaint以其卓越的性能和创新的技术引起了广泛关注。 一、为何选择预测性维护&#xff1f; 传统的维护方法&#xff0c;基于固定的时间表&#xff0c;无法灵活应对设备的真实运行状况。…

金融行业现场故障处理实录

KL银行现场服务记录—HA故障 服务时间 2019年9月10日星期二 14&#xff1a;40 到2019年9月11日星期三 0&#xff1a;30 服务内容 排查redhat RHEL 6.4 一个节点cman启动故障。 &#xff08;1&#xff09;、查看系统日志&#xff1b; &#xff08;2&#xff09;、查看ha日志…

编程大侦探林浩然的“神曲奇遇记”

编程大侦探林浩然的“神曲奇遇记” The Coding Detective Lin Haoran’s “Divine Comedy Adventures” 在我们那所充满活力与创新精神的高职学院中&#xff0c;林浩然老师无疑是众多教师中最独特的一颗星。这位身兼程序员与心理分析专家双重身份的大咖&#xff0c;不仅能在电脑…

APPium简介及安装

1 APPium简介 1. 什么是APPium&#xff1f; APPium是一个开源测试自动化框架&#xff0c;适用于原生、混合或移动Web应用程序的自动化测试工具。 APPium使用WebDriver协议驱动iOS、Android等应用程序。 2. APPium的特点 支持多平台&#xff08;Android、iOS等&#xff09; …

浅谈楼房老旧的配电设备加装电能管理系统的方案

摘要&#xff1a;文章通过对大楼配电设备现状及电能管理系统的需求分析&#xff0c;提出了在大楼老旧配电设备中加装 电能管理系统的方法&#xff0c;包括方案配置、计量点选择、终端改造、数据通信、报表格式等。旨在供无计量 管理系统或仅有电力监控系统的配电系统中加装电能…

目标检测数据集制作(VOC2007格式数据集制作和处理教程)

VOC2007数据集结构&#xff08;目标检测图像分割&#xff09; #VOC2007数据集结构如下&#xff1a; VOC2007|-Annotations#里面存放的是每一张图片对应的标注结果&#xff0c;为XML文件&#xff0c;#标注完成后JPEGImages每张图片在此都有一一对应的xml文件|-ImageSets#存放的是…

论文笔记:TimeGPT-1

时间序列的第一个基础大模型 1 方法 最basic的Transformer架构 采用了公开可用的最大时间序列数据集进行训练&#xff0c;包含超过1000亿个数据点。 训练集涵盖了来自金融、经济、人口统计、医疗保健、天气、物联网传感器数据、能源、网络流量、销售、交通和银行业等广泛领域…

人工智能趋势报告解读:ai野蛮式生长的背后是机遇还是危机?

近期&#xff0c;Enterprise WordPress发布了生成式人工智能在营销中的应用程度的报告&#xff0c;这是一个人工智能迅猛发展的时代&#xff0c;目前人工智能已经广泛运用到内容创作等领域&#xff0c;可以预见的是人工智能及其扩展应用还将延伸到我们工作与生活中的方方面面。…

【C++】C++入门基础讲解(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 导读 接着上一篇的内容继续学习&#xff0c;今天我们需要重点学习引用。 1. 引用 在C中&#xff0c;引用是一种特殊的变量&#xff…