首页 > 编程语言 >Offer必备算法23_两个数组dp_八道力扣题详解(由易到难)

Offer必备算法23_两个数组dp_八道力扣题详解(由易到难)

时间:2024-04-09 20:29:47浏览次数:33  
标签:匹配 Offer s2 由易到难 23 空串 字符串 s1 dp

目录

①力扣1143. 最长公共子序列

解析代码

②力扣1035. 不相交的线

解析代码

③力扣115. 不同的子序列

解析代码

④力扣44. 通配符匹配

解析代码

⑤力扣10. 正则表达式匹配

解析代码

⑥力扣97. 交错字符串

解析代码

⑦力扣712. 两个字符串的最小ASCII删除和

解析代码

⑧力扣718. 最长重复子数组

解析代码

本篇完。


①力扣1143. 最长公共子序列

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 仅由小写英文字符组成。
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {

    }
};

解析代码

状态表示:

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

在这道题中,根据题目定义状态表示为:

dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,最长公共子序列的长度


状态转移方程:

        分析状态转移方程的经验就是根据最后一个位置的状况,分情况讨论。 对于 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] 结尾。那么我们找最长公共子序列时,有下面三种策略:
  1. 去 s1 的 [0, i - 1] 以及 s2 的 [0, j] 区间内找:此时最大长度为 dp[i- 1][j] 。
  2. 去 s1 的 [0, i] 以及 s2 的 [0, j - 1] 区间内找:此时最大长度为 dp[i ][j - 1] 。
  3. 去 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]) ;

初始化、填表顺序、返回值:

        初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。 引入空串后,大大的方便我们的初始化。 但也要注意下标的映射关系,以及里面的值要保证后续填表是正确的。 当 s1 为空时,没有长度,同理 s2 也是。因此第一行和第一列里面的值初始化为 0 即可保证后续填表是正确的,还可以通过在s1和s2最前面加上一个字符来对应下标的映射

填表顺序:从上往下填写每一行,每一行从左往右,最后返回dp[m][n]。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        // dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间
        // 内的所有的子序列中,最长公共子序列的长度
        int m = text1.size(), n = text2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        // text1 = " " + text1, text2 = " " + text2; // 注释掉,在原数组找i,j就要-1
        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][j - 1], dp[i - 1][j]);
            }
        }
        return dp[m][n];
    }
};


②力扣1035. 不相交的线

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) {

    }
};


解析代码

        如果要保证两条直线不相交,那么我们下一个连线必须在上一个连线对应的两个元素的后面寻找相同的元素。这不就转化成上一题最长公共子序列的模型了吗。那就是在这两个数组中寻找最长的公共子序列。 只不过是在整数数组中做一次最长的公共子序列,代码几乎一模一样:

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, 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;
                else
                    dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
        return dp[m][n];
    }
};

③力扣115. 不同的子序列

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 由英文字母组成
class Solution {
public:
    int numDistinct(string s, string t) {

    }
};

解析代码

状态表示:

对于两个字符串之间的 dp 问题,一般的思考方式如下:

        选取第一个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义状态表示。然后根据两个区间上最后一个位置的字符,来进行分类讨论,从而确定状态转移方程。

dp[i][j] 表示:在字符串 s 的 [0, j] 区间内的所有子序列中,有多少个 t 字符串 [0, i] 区间内的子串


状态转移方程: 根据最后一个位置的元素,结合题目要求,分情况讨论:

  • t[i] == s[j] 的时候,此时的子序列有两种选择:
  1. 一种选择是:子序列选择 s[j] 作为结尾,此时相当于在状态 dp[i - 1][j - 1] 中的所有符合要求的子序列的后面,再加上⼀个字符 s[j] (请结合状态表示好好理解这句话),此时 dp[i][j] = dp[i - 1][j - 1] ;
  2. 另⼀种选择是:子序列不选择 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] ;


初始化、填表顺序、返回值:

        初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。 引入空串后,大大的方便我们的初始化。 但也要注意下标的映射关系,以及里面的值要保证后续填表是正确的。当 s 为空时, t 的子串中有一个空串和它⼀样,因此初始化第一行全部为 1 。还可以通过在s1和s2最前面加上一个字符来对应下标的映射

填表顺序:从上往下填写每一行,每一行从左往右,最后返回dp[m][n]。

class Solution {
public:
    int numDistinct(string s, string t) {
        // dp[i][j] 表示:在字符串 s 的 [0, j] 区间内的所有子序列中,
        // 有多少个 t 字符串 [0, i] 区间内的子串
        int m = t.size(), n = s.size();
        vector<vector<double>> dp(m + 1, vector<double>(n + 1, 0)); // 防溢出
        for(int j = 0; j <= n; ++j)
            dp[0][j] = 1;

        for(int i = 1; i <= m; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] += dp[i][j - 1];
                if(t[i - 1] == s[j - 1]) 
                    dp[i][j] += dp[i - 1][j - 1];
            }
        }
        return dp[m][n];
    }
};


④力扣44. 通配符匹配

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 仅由小写英文字母、'?' 或 '*' 组成
class Solution {
public:
    bool isMatch(string s, string p) {

    }
};

解析代码

状态表示:

对于两个字符串之间的 dp 问题,我们一般的思考方式如下:

        选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义状态表示。然后根据两个区间上最后一个位置的字符,来进行分类讨论,从而确定状态转移方程。

dp[i][j] 表示: p 字符串 [0, j] 区间内的子串能否匹配字符串 s 的 [0, i] 区间内的子串。


状态转移方程:

根据最后一个位置的元素,结合题目要求,分情况讨论:

  • 当 s[i] == p[j] 或 p[j] == '?的时候,此时两个字符串匹配上了当前的一个字符,只能从 dp[i - 1][j - 1] 中看当前字符前面的两个子串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i - 1[j - 1] ;
  • 当 p[j] == '*' 的时候,此时匹配策略有两种选择:
  1. 一种选择是: * 匹配空字符串,直接继承状态 dp[i][j - 1] ,此时 dp[i][j] = dp[i][j - 1] ;
  2. 另一种选择是: * 向前匹配 1 ~ n 个字符,直至匹配上整个 s 串。此时相当于从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的情况。此时 dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;
  • 当 p[j] 不是特殊字符,且不与 s[i] 相等时,无法匹配,p[i][j] = flase

三种情况加起来,就是所有可能的匹配结果。综上所述,状态转移方程为:

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

这个状态转移方程时间复杂度为O(N^3),要想想优化。


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

当 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][j - 1] || dp[i - 1][j]。


初始化、填表顺序、返回值:

初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为false 。由于需要用到前一行和前一列的状态,初始化第一行、第一列即可。

dp[0][0] 表示两个空串能否匹配,答案是显然的, 初始化为 true 。

第一行表示 s 是一个空串, p 串和空串只有一种匹配可能,即 p 串表示为 "***" ,此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "*" 的 p 子串和空串的 dp 值设为 true 。

第一列表示 p 是一个空串,不可能匹配上 s 串,跟随数组初始化成false即可。

填表顺序:从上往下填写每一行,每一行从左往右,最后返回dp[m][n]。

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        // dp[i][j] 表示: p字符串[0, j]区间内的子串能否匹配字符串s的[0, i]区间内的子串
        dp[0][0] = true;
        for(int j = 1; j <= n; ++j)
        {
            if(p[j] == '*')
                dp[0][j] = true;
            else
                break;
        }

        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(s[i] == p[j] || p[j] == '?')
                    dp[i][j] = dp[i - 1][j - 1];
                else if(p[j] == '*')
                    dp[i][j] = (dp[i][j - 1] || dp[i - 1][j]);
            }
        }
        return dp[m][n];
    }
};


⑤力扣10. 正则表达式匹配

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 的小写字母,以及字符 . 和 *
  • 保证每次出现字符 * 时,前面都匹配到有效的字符
class Solution {
public:
    bool isMatch(string s, string p) {

    }
};

解析代码

状态表示:

对于两个字符串之间的 dp 问题,一般的思考方式如下:

        选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义状态表示。然后根据两个区间上最后一个位置的字符,来进行分类讨论,从而确定状态转移方程。

dp[i][j] 表示:字符串 p 的 [0, j] 区间和字符串 s 的 [0, i] 区间是否可以匹配。


状态转移方程:

根据最后一个位置的元素,结合题目要求,分情况讨论:

  • 当 p[j] 不是特殊字符,且不与 s[i] 相等时,无法匹配。
  • 当 s[i] == p[j] 或 p[j] == '.' 的时候,此时两个字符串匹配上了当前的一个字符, 只能从 dp[i - 1][j - 1] 中看当前字符前面的两个子串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i - 1][j - 1] ;
  • b. 当 p[j] == '*' 的时候,和力扣44. 通配符匹配稍有不同的是,上道题 "*" 本身便可匹配 0 ~ n 个字符,但此题是要带着 p[j - 1] 的字符⼀起,匹配 0 ~ n 个和 p[j - 1] 相同的字符。此时,匹配策略有两种选择:
  1. 一种选择是: p[j - 1]* 匹配空字符串,直接继承状态 dp[i][j - 2] ,此时 dp[i][j] = dp[i][j - 2] ;
  2. 另一种选择是: p[j - 1]* 向前匹配 1 ~ n 个字符(与力扣44. 通配符匹配不同,此时p[j - 1]与s[i] 要相等 或者 p[j - 1] 为点),直至匹配上整个 s 串。此时相当于从 dp[k][j - 2] (0 < k <= i) 中所有匹配情况中,选择性继承可以成功的情况。此时 dp[i][j] = dp[k][j - 2] (0 < k <= i 且 s[k]~s[i] = p[j - 1]) ;

三种情况加起来,就是所有可能的匹配结果。 综上所述,状态转移方程为:

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

这个状态转移方程时间复杂度为O(N^3),要想想优化。


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

当 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 - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1] ......

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


初始化、填表顺序、返回值:

初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为false 。由于需要用到前一行和前一列的状态,初始化第一行、第一列即可。

dp[0][0] 表示两个空串能否匹配,答案是显然的, 初始化为 true 。

第一行表示 s 是一个空串, p 串和空串只有一种匹配可能,即 p 串表示为 "任一字符+*" ,此时也相当于空串匹配上空串。所以可以遍历 p 串,把所有前导为 "任一字符+*" 的 p 子串和空串的 dp 值设为 true 。

第一列表示 p 是一个空串,不可能匹配上 s 串,跟随数组初始化成false即可。

填表顺序:从上往下填写每一行,每一行从左往右,最后返回dp[m][n]。

class Solution {
public:
    bool isMatch(string s, string p) {
        // dp[i][j]表示字符串p的[0, j]区间和字符串s的[0, i]区间是否可以匹配
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        dp[0][0] = true;
        for(int j = 2; j <= n; j += 2)
        {
            if(p[j] == '*')
                dp[0][j] = true;
            else
                break;
        }
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(s[i] == p[j] || p[j] == '.')
                {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                else if(p[j] == '*')
                {   // j-1为点 或者 和s[i]相等才可以匹配dp[i - 1][j]
                    if(p[j - 1] == '.' || p[j - 1] == s[i])
                        dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
                    else // 匹配空串的
                        dp[i][j] = dp[i][j - 2];
                }
            }
        }
        return dp[m][n];
    }
};


⑥力扣97. 交错字符串

97. 交错字符串

难度 中等

给定三个字符串 s1s2s3,请你帮忙验证 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
  • s1s2、和 s3 都由小写英文字母组成

进阶:您能否仅使用 O(s2.length) 额外的内存空间来解决它?

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {

    }
};

解析代码

状态表示:对于两个字符串之间的 dp 问题,一般的思考方式如下:

        选取第一个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义状态表示。然后根据两个区间上最后一个位置的字符,来进行分类讨论,从而确定状态转移方程。

dp[i][j] 表示字符串 s1 中 [1, i] 区间内的字符串以及 s2 中 [1, j] 区间内的字符串,能否拼接成 s3 中 [1, i + j] 区间内的字符串。


状态转移方程:

        先分析一下题目,题目中交错后的字符串为 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] ; 此时 dp[i][j] = dp[i][j - 1] ;
  • 当两者的末尾都不等于 s3 最后一个位置的字符时,说明不可能是两者的交错字符串。 dp[i][j] = false;

上两种情况下,只要有一个情况下能够交错组成目标串,就可以返回 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]) ; 只要有一个成立,结果就是 true 。


初始化、填表顺序、返回值:

初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为false 。由于需要用到前一行和前一列的状态,初始化第一行、第一列即可。

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 的⻓度)

填表顺序:从上往下填写每一行,每一行从左往右,最后返回dp[m][n]。

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int m = s1.size(), n = s2.size();
        if(m + n != s3.size())
            return false;
        s1 = " " + s1, s2 = " " + s2, s3 = " " + s3;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        // dp[i][j] 表⽰字符串s1中[1, i]区间内的字符串以及 
        // s2中[1, j]区间内的字符串,能否拼接成s3中[1, i + j]区间内的字符串
        dp[0][0] = true;
        for(int j = 1; j <= n; ++j) // 第一行,s1为空
        {
            if(s2[j] == s3[j])
                dp[0][j] = true;
            else
                break;
        }
        for(int i = 1; i <= m; ++i) // 第一列,s2为空
        {
            if(s1[i] == s3[i])
                dp[i][0] = true;
            else
                break;
        }
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j])
                        || (s2[j] == s3[i + j] && dp[i][j - 1]);
                // 也可以用下面注释
                // if(s1[i] == s3[i + j])
                // {
                //     dp[i][j] = dp[i - 1][j];
                //     if(dp[i][j] == true)
                //         continue;
                // }
                // if(s2[j] == s3[i + j])
                //     dp[i][j] = dp[i][j - 1];
            }
        }
        return dp[m][n];
    }
};


⑦力扣712. 两个字符串的最小ASCII删除和

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 由小写英文字母组成
class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {

    }
};

解析代码

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

状态表示:对于两个字符串之间的 dp 问题,一般的思考方式如下:

        选取第一个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义状态表示。然后根据两个区间上最后一个位置的字符,来进行分类讨论,从而确定状态转移方程。

dp[i][j] 表示 s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,公共子序列的 ASCII 最大和。


状态转移方程:

对于 dp[i][j] 根据最后一个位置的字符,结合题目要求,来进行分类讨论:

  • 当 s1[i] == s2[j] 时:应该先在 s1 的 [0, i - 1] 区间以及 s2 的 [0, j - 1] 区间内找一个公共子序列的最大和,然后在它们后面加上一个 s1[i]字符即可。 此时dp[i][j] = max(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] = max(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]) ;

初始化:空串是有研究意义的,因此我们将原始 dp 表的规模多加上一行和一列,表示空串。当 s1 为空时,没有长度,同理 s2 也是。因此第一行和第一列里面的值初始化为 0 即可保证后续填表是正确的。

填表顺序:从上往下填写每一行,每一行从左往右。

返回值:找到dp[m][n],,也是最大公共 ASCII 和,然后统计两个字符串的 ASCII 码和 sum,最后返回sum - 2 * dp[m][n]

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int sum = 0;
        for(auto& e : s1)
            sum += e;
        for(auto& e : s2)
            sum += e;
        int m = s1.size(), n = s2.size();
        s1 = " " + s1, s2 = " " + s2;
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        // dp[i][j]表示s1的[0, i]区间以及s2的[0, j]区间内的所有的子序列中,
        // 公共子序列的 ASCII 最大和。
        for(int i = 1; i <= m; ++i)
        {
            for(int j = 1; j <= n; ++j)
            {
                if(s1[i] == s2[j])
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + s1[i]);
                else // ((s1[i] != s2[j]))
                    dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
        return sum - 2 * dp[m][n];
    }
};


⑧力扣718. 最长重复子数组

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
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {

    }
};

解析代码

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

状态表示:

        结合题目得到:dp[i][j] 表示以第一个数组的 i 位置为结尾,以及第二个数组的 j 位置为结尾公共的 、长度最长的子数组的长度。

状态转移方程:

        对于 dp[i][j] ,当 nums1[i] == nums2[j] 的时候,此时最长重复子数组的长度应该等于 1 加上除去最后一个位置时,以 i - 1, j - 1 为结尾的最长重复子数组的长度。 因此,状态转移方程为: dp[i][j] = 1 + dp[i - 1][j - 1],nums1[i] != nums2[j] 时 为 0,所以可以初始化dp表为0。

初始化:

        为了处理越界的情况,可以添加一行和一列, dp 数组的下标从 1 开始,这样就无需初始化。 第一行表示第⼀个数组为空,此时没有重复子数组,因此里面的值设置成 0 即可, 第一列也是同理。

填表顺序:从上往下填写每一行,每一行从左往右。

最后返回dp表的最大值。

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size(), ret = 0;
        vector<vector<int>> dp(m + 1,vector<int>(n + 1, 0));
        //dp[i][j] 表示以第一个数组的 i 位置为结尾,
        // 以及第二个数组的 j 位置为结尾公共的 、长度最长的子数组的长度。
        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(ret, dp[i][j]);
                }
            }
        }
        return ret;
    }
};


本篇完。

下一篇是BFS解决FloodFill算法。

下下篇动态规划类型的是背包问题中的01背包类型的OJ。

标签:匹配,Offer,s2,由易到难,23,空串,字符串,s1,dp
From: https://blog.csdn.net/GRrtx/article/details/136653220

相关文章

  • 【专题】2023年全球数字消费者趋势调查中国篇报告合集PDF分享(附原数据表)
    原文链接:https://tecdat.cn/?p=35727原文出处:拓端数据部落公众号数字消费者已成为市场的重要驱动力。他们通过互联网、移动设备等渠道获取信息、购买商品、进行社交,引领着消费市场的变革。中国数字消费者具有网络普及率高、追求个性化消费体验等特点,同时也面临网络安全、数据泄......
  • 2023-2024学年AILD劳动技能大赛初赛报名启动
    报名方法一:登录大赛官网aild.org.cn,点击首页在线报名图标进入报名系统。报名方法二:微信扫描下方二维码直接进入报名系统。......
  • 瑞_23种设计模式_备忘录模式(快照模式)
    文章目录1备忘录模式(MementoPattern)★1.1介绍1.2概述1.3备忘录模式的结构1.4备忘录模式的优缺点1.5备忘录模式的使用场景2案例一2.1需求2.2“白箱”备忘录模式2.3“黑箱”备忘录模式★★★3案例二3.1需求3.2代码实现......
  • 猫头虎博主深度探索:Amazon Q——2023 re:Invent 大会的 AI 革新之星
    摘要大家好,我是猫头虎博主!今天,我要带大家深入了解2023年re:Invent大会上发布的一款革命性产品——AmazonQ。让我们一起探索这个引领未来工作方式的新型工具吧!引言在2023年的re:Invent大会上,亚马逊云科技(亚马逊云科技)不仅展示了包括AmazonGraviton3、AmazonSageMakerSt......
  • 题解:P10234 [yLCPC2024] B. 找机厅
    题意简述给你一个长\(n\)宽\(m\)的\(01\)迷宫,从\((1,1)\)开始要走到\((n,m)\)。如果能走那么输出最短路和路径(路径用\(LRUD\)表示),否则输出\(-1\)。有\(t\)组数据。如果当前格子是\(0\)那么你只能走到\(1\)的格子,反之亦然。思路考虑使用\(BFS\),每次走......
  • 20天【代码随想录算法训练营34期】第六章 二叉树part07 ( ● 530.二叉搜索树的最小绝对
    530.二叉搜索树的最小绝对差#Definitionforabinarytreenode.#classTreeNode:#def__init__(self,val=0,left=None,right=None):#self.val=val#self.left=left#self.right=rightclassSolution:deftraversal(self,......
  • 2024年第 6 期《Python 测试平台开发》进阶课程(4月23号开学)
    2024年第6期《Python测试平台开发》进阶课程主讲老师:上海-悠悠上课方式:微信群视频在线教学,方便交流本期上课时间:4月23号(周二、四晚上21:00-22:30)报名费:报名费3800一人(之前学过《python接口+测试开发》课程的同学可优惠!)联系微信/QQ:283340479课程环境:1.pycharm+python3.......
  • 低功耗抗噪/高抗干扰,LCD显示驱动控制电路(IC)-VK2C23A/B LQFP48/64 替代市面上16C23,56*4
    VK2C23是一个点阵式存储映射的LCD驱动器,可支持最大224点(56SEGx4COM)或者最大416点(52SEGx8COM)的LCD屏。单片机可通过I2C接口配置显示参数和读写显示数据,也可通过指令进入省电模式。其高抗干扰,低功耗的特性适用于水电气表以及工控仪表类产品。L23+01特点:•工作电压2.4-5.5V•内......
  • HZ2023 远足游记
    你说得对,但是我放假之前写的P4689代码没了所以来摆4.6(远足)上午走路,刚开始感觉没啥走到园博园发现没预料中那么顺利但是还感觉没啥因为也没预料到\(N·m\)学校会让我们原路返回,而不是直接走最近的路......
  • 界面组件DevExpress WinForms v23.2 - 功能区、富文本编辑器功能升级
    DevExpressWinForms拥有180+组件和UI库,能为WindowsForms平台创建具有影响力的业务解决方案。DevExpressWinForms能完美构建流畅、美观且易于使用的应用程序,无论是Office风格的界面,还是分析处理大批量的业务数据,它都能轻松胜任!DevExpressWinForms控件日前正式发布了v23.2,此版......