动态规划六——两个数组的dp问题

目录

[题目一------1143. 最长公共子序列 - 力扣(LeetCode)](#题目一——1143. 最长公共子序列 - 力扣(LeetCode))

[题目二------1035. 不相交的线 - 力扣(LeetCode)](#题目二——1035. 不相交的线 - 力扣(LeetCode))

[题目三------115. 不同的子序列 - 力扣(LeetCode)](#题目三——115. 不同的子序列 - 力扣(LeetCode))

[题目四------ 44. 通配符匹配 - 力扣(LeetCode)](#题目四—— 44. 通配符匹配 - 力扣(LeetCode))

[题目五------10. 正则表达式匹配 - 力扣(LeetCode)](#题目五——10. 正则表达式匹配 - 力扣(LeetCode))

[题目六------97. 交错字符串 - 力扣(LeetCode)](#题目六——97. 交错字符串 - 力扣(LeetCode))

[题目七------712. 两个字符串的最小ASCII删除和 - 力扣(LeetCode)](#题目七——712. 两个字符串的最小ASCII删除和 - 力扣(LeetCode))

[题目八------718. 最长重复子数组 - 力扣(LeetCode)](#题目八——718. 最长重复子数组 - 力扣(LeetCode))


题目一------1143. 最长公共子序列 - 力扣(LeetCode)

这题是经典中的经典!!!

1.状态表示:

对于两个数组的动态规划,我们的定义状态表示的经验就是:

  • i. 选取第一个数组[0, i]区间以及第二个数组[0, j]区间。
  • ii. 结合题目要求,定义状态表示。

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

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

2.状态转移方程:

分析状态转移方程的经验就是根据「最后一个位置」的状况,分情况讨论。

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

i. 两个字符相同,即s1[i] = s2[j]: 那么最长公共子序列就在s1的[0, i - 1]区间以及s2的[0, j - 1]区间上找到一个最长的,然后再加上s1[i]即可。因此dp[i][j] = dp[i - 1][j - 1] + 1;

ii. 两个字符不相同,即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]。

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

综上,状态转移方程为:

  • 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])。

3.初始化:

我们知道状态转移方程会使用dp[i - 1][j - 1],那对于i==0和j==0的情况,那这个dp[i - 1][j - 1]不是会越界吗,为了防止这个情况的发生, 我们可以将dp表多开一行一列,但是这样子会影响到和原数组的下标映射关系,所以我们可以在原字符串前加一个空字符,这样子就不会有下标映射的困扰了

此外,当s1为空时,没有长度,同理s2也是。因此dp表里面第一行和第一列里面的值初始化为0即可保证后续填表是正确的。

  • a. 「空串」是有研究意义的,因此我们将原始dp表的规模多加上一行和一列,表示空串。
  • b. 引入空串后,大大的方便我们的初始化。
  • c. 但也要注意「下标的映射关系」,以及里面的值要「保证后续填表是正确的」。

4.填表顺序:

根据「状态转移方程」得:从上往下填写每一行,每一行从左往右。

5.返回值:

根据「状态表示」得:返回dp[m][n]。


代码如下:

cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m=text1.size(),n=text2.size();
        
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        
        text1=' '+text1;
        text2=' '+text2;
        
        int ret=0;
        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]);
                }
                ret=max(ret,dp[i][j]);
            }
        }
        return ret;
    }
};

题目二------1035. 不相交的线 - 力扣(LeetCode)

**如果要保证两条直线不相交,那么我们「下⼀个连线」必须在「上⼀个连线」对应的两个元素的 「后⾯」寻找相同的元素。**这不就转化成「最⻓公共⼦序列」的模型了嘛。

那就是在这两个数组中 寻找「最⻓的公共⼦序列」。 只不过是在整数数组中做⼀次「最⻓的公共⼦序列」,代码⼏乎⼀模⼀样,这⾥就不再赘述算法原理啦~

cpp 复制代码
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));
        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;
                }
                else
                {
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                }
                ret=max(ret,dp[i][j]);
            }
        }
        return ret;
    }
};

题目三------115. 不同的子序列 - 力扣(LeetCode)

1.状态表示

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

  • i. 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的[0, j] 区间当成研究对象,结合题⽬的要求来定义「状态表⽰」;
  • ii. 然后根据两个区间上「最后⼀个位置的字符」,来进⾏「分类讨论」,从⽽确定「状态转移 ⽅程」。

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

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

2.状态转移方程

接下来,我们根据两个区间上"**最后一个位置的字符"**来进行分类讨论,以确定状态转移方程。

  • t[i] == s[j] 时,此时的子序列有两种选择:

    • 一种选择是选择s[i]为结尾:这意味着问题就是在字符串 s[0, j] 区间内的所有子序列中,有多少个 t字符串 的 [0, i] 区间的子序列。这意味着,在 dp[i - 1][j - 1] 中的所有符合要求的子序列的后面,再加上一个字符 s[j]。因此,dp[i][j] = dp[i - 1][j - 1]

    • 另一种选择是不选择s[i]为结尾:这意味着问题就是在字符串 s[0, j-1] 区间内的所有子序列中,有多少个 t字符串 的 [0, i] 区间的子序列。这意味着,我们直接继承了 dp[i][j - 1] 中所有符合要求的子序列。因此,dp[i][j] = dp[i][j - 1]

    • 综合两种情况,当 t[i] == s[j] 时,dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]。

  • t[i] != s[j] 时,由于 s[j]t[i] 不匹配,此时的子序列只能从 dp[i][j - 1] 中选择所有符合要求的子序列。也就是说,我们只能继承上一个状态中求得的子序列。因此,dp[i][j] = dp[i][j - 1]。

综上所述,状态转移方程可以总结为:

  • 在所有情况下,dp[i][j] 都可以继承上一次的结果,即 dp[i][j] = dp[i][j - 1]
  • t[i] == s[j] 时,除了继承上一次的结果外,还可以多选择一种情况,即 dp[i][j] += dp[i - 1][j - 1]

3.初始化

我们知道状态转移方程会出现j-1或者i-1,那当j=0或者i=0的时候那不是会越界吗?这可不行,所以我们需要另外开一行一列,这样子就能防止越界情况的发生了。

只不过这样子的新dp表的下标和原数组的下标是对不上的。是需要经过-1才能得到原数组的下标的。为了避免这种麻烦,我们直接给两个字符串的最前面加上一个空字符串' ',这样就可以免去下标映射的痛苦。

此外,当 s 为空时, t 的⼦串中有⼀个空串和它⼀样,因此初始化第⼀⾏全部为 1 。

4.填表顺序

我们看状态转移方程 dp[i][j] += dp[i - 1][j - 1]

这就决定了dp[i - 1][j - 1]一定比dp[i][j]先出现,所以是从左往右,从上往下。

5.返回值

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

所以我们返回dp[t.size()][s.size()]。即可


本题有⼀个巨恶⼼的地⽅,题⽬上说结果不会超过 int 的最⼤值,但是实际在计算过程会会超。为了避免报错,我们选择 double 存储结果。

cpp 复制代码
class Solution {
public:
    int numDistinct(string s, string t) {
        int n=s.size(),m=t.size();
        vector<vector<double>>dp(m+1,vector<double>(n+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++)
            {
                if(s[j]==t[i])
                {
                    dp[i][j]=dp[i][j-1]+dp[i-1][j-1];
                }
                else
                {
                    dp[i][j]=dp[i][j-1];
                }
            }
        }
        return dp[m][n];
    }
};

题目四------ 44. 通配符匹配 - 力扣(LeetCode)

嗯?这个和我们的Linux里面的通配符是一模一样的啊!!!

1.状态表示

对于两个字符串之间的DP问题,我们通常的思考方式如下:

  • 将第一个字符串 s[0, i] 区间和第二个字符串 p[0, j] 区间作为研究对象。
  • 定义状态 dp[i][j] 来表示某种关系或属性,这个关系或属性通常与 s 的前 i+1 个字符和 p 的前 j+1 个字符有关。

在这个上下文中,我们可以定义 dp[i][j] 为:

  • dp[i][j] 表示 s 的【0,i】区间的字符串能否与 p 的【0,j】区间的字符串相匹配。

2.推导状态转移方程

首先啊,这种题目都是在最后一个字符做文章啊!!!

一共有三种情况

  1. 当 s[i] == p[j]或者p[j] == '?' 的时候
  2. p[j] == '*' 的时候
  3. 当 p[j] 不是特殊字符,且不与 s[i] 相等时

当 s[i] == p[j]或者p[j] == '?' 的时候,此时两个字符串匹配上了当前的⼀个字 符,只能从 dp[i - 1][j - 1] 中看当前字符前⾯的两个⼦串是否匹配。只能继承上个 状态中的匹配结果, dp[i][j] = dp[i-1][j - 1] ;


当 p[j] == '*' 的时候,此时匹配策略有多种情况:

* 匹配空字符串,此时相当于它匹配了一个寂寞,直接继承状态 dp 数组的前一个状态,即 dp[i][j-1],此时 dp[i][j] = dp[i][j - 1]

* 匹配s字符串里的一个字符,直接继承状态 dp 数组的前一个状态,即dp[i-1][j-1],此时dp[i][j]=dp[i-1][j-1]

* 匹配s字符串里的两个字符时,,直接继承状态 dp 数组的前一个状态,即dp[i-2][j-1],此时dp[i][j]=dp[i-2][j-1]

那么匹配3个字符,4个字符......啥的就不说了啊!!

* 向前匹配 1 ~ n 个字符,直⾄匹配上整个 s 串。此时相当于 从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的 情况。此时 dp[i][j] = dp[k][j - 1](0 <= k <= i)

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

在这里,我们发现只要这个dp[i][j] = dp[k][j - 1](0 <= k <= i) ,只要dp[i][j] = dp[k][j - 1](0 <= k <= i) 里面任何一个是true,那么dp[i][j]就是true。

当 p[j] == '*' 时,状态转移⽅程为:dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1] ......

但是当我们不断的修改i的值,我们就会发现下面这个情况

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

那不就很简单了吗?循环也省去了。


当 p[j] 不是特殊字符,且不与 s[i] 相等时,⽆法匹配。


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

  1. 当 s[i] == p[j]或者p[j] == '?' 的时候:此时dp[i][j]=dp[i-1][j-1]
  2. p[j] == '*' 的时候,此时dp[i][j]=dp[i-1][j]||dp[i][j-1]
  3. 当 p[j] 不是特殊字符,且不与 s[i] 相等时,此时dp[i][j]=false;

3. 初始化:

我们知道状态转移方程会使用dp[i - 1][j - 1],那对于i==0和j==0的情况,那这个dp[i - 1][j - 1]不是会越界吗,为了防止这个情况的发生, 我们可以将dp表多开一行一列,但是这样子会影响到和原数组的下标映射关系,所以我们可以在原字符串前加一个空字符,这样子就不会有下标映射的困扰了

为了防止大家搞混和原字符串的下标映射关系,我们可以在原字符串前面加一个空字符。这样子下标映射就没有问题了。

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

  • dp[0][0] 表⽰两个空串能否匹配,答案是显然的,初始化为 true 。
  • 第⼀⾏(dp[0][j])表⽰ s 是⼀个空串, p 串和空串只有⼀种匹配可能,即 p 串表⽰为"***" ,此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "*" 的p ⼦串和空串的 dp 值设为 true 。
  • 第⼀列表⽰ p 是⼀个空串,不可能匹配上 s 串,跟随数组初始化即可。

4. 填表顺序:

从上往下填每⼀⾏,每⼀⾏从左往右。

5. 返回值:

根据状态表⽰,返回 dp[m][n] 的 值。


cpp 复制代码
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[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-1][j]||dp[i][j-1];
                }
            }
        }
        return dp[m][n];
    }
};

题目五------10. 正则表达式匹配 - 力扣(LeetCode)

我们仔细看一下题目

我觉得大家最不理解的就是这个*,我来讲一下

  • *不能单独存在,只能和它前面那个字符搭配使用
  • a* 表示 a 可以出现零次或多次
  • 题目保证每次出现字符 * 时,前面都匹配到有效的字符

1.状态表示

对于两个字符串之间的DP问题,我们通常的思考方式如下:

  • 将第一个字符串 s[0, i] 区间和第二个字符串 p[0, j] 区间作为研究对象。
  • 定义状态 dp[i][j] 来表示某种关系或属性,这个关系或属性通常与 s 的前 i+1 个字符和 p 的前 j+1 个字符有关。

在这个上下文中,我们可以定义 dp[i][j] 为:

  • dp[i][j] 表示 s 的【0,i】区间的字符串能否与 p 的【0,j】区间的字符串相匹配。

2.推导状态转移方程

首先啊,这种题目都是在最后一个字符做文章啊!!!

一共有三种情况

  1. 当 s[i] == p[j]或者p[j] == '.' 的时候
  2. p[j] == '*' 的时候
  3. 当 p[j] 不是特殊字符,且不与 s[i] 相等时

当 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] 相同的字 符。

按照上题的思路,我们可能会像下面这样子干

p[j-1] 变成空字符串* ,这个时候我不需要管你s[i]是什么,我直接将p[j-1]*设置为一个空串,此时相当于它匹配了一个寂寞,直接继承状态 dp 数组的前一个状态,即 dp[i][j-2],此时dp[i][j] = dp[i][j - 2];

p[j-1]匹配s字符串里的一个字符时,此时p[j-1]==s[i]或者p[j-1]=='.' ,直接继承状态 dp 数组的前一个状态,即dp[i-1][j-2],此时dp[i][j]=dp[i-1][j-2]*

p[j-1] 匹配s字符串里的两个字符时,此时p[j-1]==s[i]或者p[j-1]=='.'* ,直接继承状态 dp 数组的前一个状态,即dp[i-2][j-2],此时dp[i][j]=dp[i-2][j-2]

那么p[j-1]* 匹配3个字符,4个字符......啥的就不说了啊!!

p[j-1]* 向前匹配 1 ~ n 个字符,直⾄匹配上整个 s 串。此时相当于 从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的 情况。此时 dp[i][j] = dp[k][j - 2](0 <= k <= i)

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

在这里,我们发现只要这个dp[i][j] = dp[k][j - 2](0 <= k <= i) ,只要dp[i][j] = dp[k][j - 2](0 <= k <= i) 里面任何一个是true,那么dp[i][j]就是true。

当 p[j] == '*' 时,状态转移⽅程为:dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1] ......

但是当我们不断的修改i的值,我们就会发现下面这个情况

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

但是后面这个dp[i-1][j]是有前提的------p[j-1] == s[i] || p[j-1] == '.'。

  • dp[i][j] = dp[i][j-2] || (dp[i-1][j] && (p[j-1] == s[i] || p[j-1] == '.'));

那不就很简单了吗?循环也省去了。


当 p[j] 不是特殊字符,且不与 s[i] 相等时,⽆法匹配。


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

  1. 当 s[i] == p[j]或者p[j] == '?' 的时候:此时dp[i][j]=dp[i-1][j-1]
  2. p[j] == '*' 的时候,此时dp[i][j] = dp[i][j-2] || (dp[i-1][j] && (p[j-1] == s[i] || p[j-1] == '.'));
  3. 当 p[j] 不是特殊字符,且不与 s[i] 相等时,此时dp[i][j]=false

3. 初始化

由于 dp 数组的值用于表示是否匹配,为了避免与答案值混淆,我们需要将整个数组初始化为 false。由于需要用到前一行和前一列的状态,我们需要特别初始化第一行和第一列。

  • dp[0][0] 表示两个空串能否匹配,答案显然是 true,因为空串与空串总是匹配的。

  • 第一行是dp[0][j],这表示 s 是一个空串,而 p 串和空串的匹配情况只有一种可能,即 p 串全部字符表示为" 任意字符 + *",此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为"任⼀字符+*" 的 p ⼦串和空串的 dp 值设为 true

  • 第一列表示 p 是一个空串,此时不可能匹配上 s 串(除非 p 本身就是一个空串或者由 * 组成的表示空串的模式,但这种情况已经在第一行处理过了),因此第一列(除了 dp[0][0])应该全部初始化为 false

4. 填表顺序

从上往下填每一行,每一行从左往右。即按照 dp[i][j] 的顺序,其中 i0ms 的长度),j0np 的长度)。

5. 返回值

根据状态表示,最终返回 dp[m][n] 的值,它表示 s 串和 p 串是否匹配。


cpp 复制代码
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[0][0] = true; 
        //本来应该是for(int i=1;i<=n;i+=2)的,但是我们p=' '+p;,
        //所以真正的第二个字符的下标是2
        for(int i=2;i<=n;i+=2)
        {
            if(p[i] == '*') 
                dp[0][i] = 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-2] || (dp[i-1][j] && (s[i] == p[j-1] || p[j-1] == '.'));
                }
            }
        }
        return dp[m][n];

    }
};

题目六------97. 交错字符串 - 力扣(LeetCode)

1.状态表示

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

  1. 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结合题⽬的要求来定义「状态表⽰」;
  2. 然后根据两个区间上「最后⼀个位置的字符」,来进⾏「分类讨论」,从⽽确定「状态转移 ⽅程」。

我们可以根据上⾯的策略,解决⼤部分关于两个字符串之间的 dp 问题。 但是这道题目有3个字符串。我们的经验还能用吗?应该是可以的

我们定了s1和s2的长度,那肯定能知道此时它们能匹配上的s3的长度。

dp[i][j] 表示字符串 s1 的【0,i】和字符串 s2 的【0,j】能否拼接成字符串 s3 的【0,i+j-1】里面的字符。

大家可能看这个i+j-1不太顺眼,所以我们可以进行一下优化。

我们先把所有原字符串进行预处理,也就是s1 = " " + s1, s2 = " " + s2, s3 = " " + s3 。

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 的⼦串。这⼀点理解,对我们的状态转移很重要。

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

(注意:下标是从1开始的)

  1. s3[i+j] = s1[i]
    • 这意味着s3的当前最后一个字符与s1的当前最后一个字符匹配。
    • 因此,为了判断整个字符串能否交错组成,我们需要检查s1的前i-1个字符与s2的前j个字符是否能交错形成s3的前i+j-1个字符,即dp[i-1][j]
    • 此时,更新规则为:dp[i][j] = dp[i-1][j](如果s1的这部分与s2的已考虑部分能匹配s3的对应部分)。
  2. s3[i+j] = s2[j]
    • 这意味着s3的当前最后一个字符与s2的当前最后一个字符匹配。
    • 因此,为了判断整个字符串能否交错组成,我们需要检查s1的前i个字符与s2的前j-1个字符是否能交错形成s3的前i+j-1个字符,即dp[i][j-1]
    • 此时,更新规则为:dp[i][j] = dp[i][j-1](如果s2的这部分与s1的已考虑部分能匹配s3的对应部分)。
  3. s3[i+j]既不等于s1[i]也不等于s2[j]
    • 这表明s3的当前最后一个字符既不能与s1的当前最后一个字符匹配,也不能与s2的当前最后一个字符匹配。
    • 因此,dp[i][j]在这种情况下必然为False,因为无法通过添加s1s2的下一个字符来形成s3的当前部分。

但是我们仔细思考一下**s3[i+j] = s1[i]和s3[i+j] = s2[j]这两种情况任意一个满足,都是符合条件的,有的人可能就会像下面这样子写**

cpp 复制代码
                // 检查s1的当前字符是否与s3的当前位置匹配,并考虑前一个状态
                if (s1[i] == s3[i + j]) {
                    dp[i][j] = dp[i - 1][j];
                }
                // 检查s2的当前字符是否与s3的当前位置匹配,并考虑前一个状态
                else if (s2[j] == s3[i + j]) {
                    dp[i][j] = dp[i][j - 1];
                }
                // 如果都不匹配,则dp[i][j]保持为false(默认初始化值)

但是这只能判断到一种情况,明显不对啊,有人做出下面这个改进

cpp 复制代码
                // 检查s1的当前字符是否与s3的当前位置匹配,并考虑前一个状态
                if (s1[i] == s3[i + j]) {
                    dp[i][j] = dp[i - 1][j];
                }
                // 检查s2的当前字符是否与s3的当前位置匹配,并考虑前一个状态
                if (s2[j] == s3[i + j]) {
                    dp[i][j] = dp[i][j - 1];
                }
                // 如果都不匹配,则dp[i][j]保持为false(默认初始化值)

那要是我s1[i]==s2[j]==s3[i+j],dp[i-1][j]是true,dp[i][j-1]是false,你不就炸了吗?

我们接着改进

cpp 复制代码
 if(s1[i] == s3[i + j]||s2[j] == s3[i + j])
{
      dp[i][j] =dp[i-1][j]||dp[i][j-1];
}

那要是我s1[i]==s3[i+j],dp[i-1][j]是false,dp[i][j-1]是true,你不就炸了吗?

接下来我告诉大家一种方法

所以我们只能是下面这个情况!

cpp 复制代码
dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j])|| (s2[j] == s3[i + j] && dp[i][j - 1]);

其实这括号里面最前面是表示这括号的值对应的情况,后面部分则是这种情况里dp[i][j]应该被赋予的值。

大家看这个式子可能有点懵,或者感觉有点晕:

  1. 当s1[i] == s3[i + j]时,s1[i] == s3[i + j]为true,s1[i] == s3[i + j]||dp[i - 1][j]的值取决于dp[i - 1][j],如果dp[i - 1][j]是true,那s1[i] == s3[i + j] && dp[i - 1][j]就是true,如果dp[i - 1][j]是false,那s1[i] == s3[i + j] && dp[i - 1][j]就是false
  2. 当s2[j] == s3[i + j]时,s2[j] == s3[i + j]为true,s2[j] == s3[i + j]||dp[i][j-1]的值取决于dp[i][j-1],如果dp[i][j-1]是true,那s1[i] == s3[i + j] && dp[i][j-1]就是true,如果dp[i][j-1]是false,那s1[i] == s3[i + j] && dp[i][j-1]就是false
  3. 中间连接部分使用||的原因是:只要上述条件中有一个成立,dp[i][j]的结果就为True,代表字符串 s1 的【1,i】和字符串 s2 的【1,j】能拼接成字符串 s3 的【1,i+j】里面的字符。

综上所述,状态转移方程可以总结为:

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

3. 初始化过程

在进行动态规划填表之前,我们需要对dp数组进行初始化。由于状态转移方程中涉及到了i-1j-1位置的值,因此我们需要特别注意"第一个位置"(即dp[0][j]dp[i][0])以及"第一行"和"第一列"的初始化。

  • dp[0][0]的初始化
    dp[0][0] = true,因为空串与空串相加仍然是一个空串,所以它们能够匹配。

  • 第一行的初始化

    s1为空串时(即i=0的情况,但注意在数组中我们用dp[0][j]来表示,其中j代表s2的【1,j】区间的字符),我们只需要考虑s2的【1,j】区间的字符是否能匹配s3的【1,j】区间的字符。状态转移方程为:dp[0][j] = (s2[j] == s3[j] && dp[0][j-1]);

  • 第一列的初始化

    s1为空串时(即j=0的情况,但注意在数组中我们用dp[i][0]来表示,其中j代表s1的【1,i】区间的字符),我们只需要考虑s1的【1,i】区间的字符是否能匹配s3的【1,i】区间的字符。状态转移方程为:dp[i][0] = (s1[i] == s3[i] && dp[i-1][0]);

4. 填表顺序

根据状态转移方程,我们需要按照"从上往下"的顺序逐行填写dp数组,而在每一行内,又需要"从左往右"地逐个填写元素。这样的填表顺序可以确保在计算每个dp[i][j]时,其所依赖的前置状态dp[i-1][j]dp[i][j-1]都已经被计算出来。

5. 返回值

最终,我们需要返回dp[m][n]的值,它表示s1的所有字符与s2的所有字符是否能够交错拼接成s3的所有字符。

cpp 复制代码
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int m = s1.size(), n = s2.size(); // 使用s1和s2的实际长度
        if (m + n != s3.size()) { return false;}
        // s3的长度应该在调用此函数之前就已经验证过是否等于m+n,但这里没有显式检查
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        
        // 为了方便索引,给s1, s2, s3前都加一个空格(相当于在dp数组中从1开始考虑字符)
        s1 = ' ' + s1;
        s2 = ' ' + s2;
        s3 = ' ' + s3;
        
        // 初始化dp数组
        dp[0][0] = true; // 空串+空串能构成空串
        
        // 初始化第一列(只考虑s1的情况)
        for (int i = 1; i <= m; ++i) {
            dp[i][0] = (s1[i] == s3[i] && dp[i - 1][0]);
        }
        
        // 初始化第一行(只考虑s2的情况)
        for (int j = 1; j <= n; ++j) {
            dp[0][j] = (s2[j] == s3[j] && dp[0][j - 1]);
        }
        
       // 填充dp数组的其余部分
        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]);
            }
        }
        
        // 返回结果
        return dp[m][n];
    }
};

题目七------712. 两个字符串的最小ASCII删除和 - 力扣(LeetCode)

正难则反:求两个字符串的最⼩ ASCII 删除和,其实就是找到两个字符串中所有的公共⼦序列 ⾥⾯, ASCII 最⼤和。

面对求解两个字符串的最小ASCII删除和的问题,我们可以转换思路,通过寻找两个字符串中的所有公共子序列,并计算这些公共子序列中的ASCII最大和来间接求解。这是因为,最小ASCII删除和实际上等于两个字符串的总ASCII码和减去公共子序列的ASCII最大和的两倍。

1.状态表示

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

2.状态转移方程

  • 当 s1[i] == s2[j] 时:

应该在 s1[0, i-1] 区间以及 s2[0, j-1] 区间内找到一个公共子序列的最大和,然后在其后添加一个 s1[i-1](或 s2[j-1],因为它们是相等的)字符。

此时,dp[i][j] = dp[i-1][j-1] + s1[i-1](注意这里 s1[i-1] 是字符,需要转换为对应的ASCII值进行计算,但为简化表述,这里仍用 s1[i-1] 表示)。

  • 当 s1[i-1] != s2[j-1] 时:

公共子序列的最大和可能存在于以下三种情况中

  • s1[0, i-1] 区间以及 s2[0, j] 区间内:dp[i][j] = dp[i-1][j]
  • s1[0, i] 区间以及 s2[0, j-1] 区间内:dp[i][j] = dp[i][j-1]
  • s1[0, i-1] 区间以及 s2[0, j-1] 区间内:dp[i][j] = dp[i-1][j-1]

但由于前两种情况已经包含了第三种情况的可能性(即不选择当前字符作为公共子序列的一部分),因此只需考虑前两种情况下的最大值。

综上,状态转移方程为:dp[i][j] = max(dp[i-1][j], dp[i][j-1])

3.初始化

为了方便处理空串的情况,我们在原始的 dp 表上额外增加一行和一列,用于表示空串。

初始化时,第一行和第一列的值都设为0,因为当其中一个字符串为空时,公共子序列的ASCII和为0。

4.填表顺序

从上往下逐行填写 dp 表,每行从左往右填写。

5.返回值

根据状态表示,我们不能直接返回 dp 表中的某个值。

首先找到 dp[m+1][n+1](注意这里 mn 分别是 s1s2 的长度,由于我们增加了额外的行和列,所以这里是 m+1n+1),它表示两个字符串的最长公共子序列的ASCII最大和。

然后统计两个字符串的总ASCII码和 sum

最后返回 sum - 2 * dp[m+1][n+1],即为所求的最小ASCII删除和。


cpp 复制代码
class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int m = s1.size(), n = s2.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(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]);
                }
            }
       int sum = 0; //统计元素和
        for(auto s : s1) sum += s;
        for(auto s : s2) sum += s;
        return sum - 2*dp[m][n];
    }
};

题目八------718. 最长重复子数组 - 力扣(LeetCode)

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

1.状态表示

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

2.状态转移方程

对于 dp[i][j],当 nums1[i-1] == nums2[j-1](注意数组下标从0开始,但dp数组下标从1开始,所以这里是 i-1j-1)的时候,才有意义。此时最长重复子数组的长度应该等于 1 加上除去最后一个位置时,以 i-1, j-1 为结尾的最长重复子数组的长度。

因此,状态转移方程为:dp[i][j] = 1 + dp[i-1][j-1](当 nums1[i-1] == nums2[j-1]);否则 dp[i][j] = 0

3.初始化

添加一行和一列,dp 数组的下标从 1 开始,这样处理起来更方便,无需额外判断越界。

第一行和第一列表示其中一个数组为空的情况,此时没有重复子数组,因此里面的值设置成 0。

4.填表顺序

根据状态转移,我们需要从上往下填每一行,每一行从左往右。

5.返回值

根据状态表示,我们需要返回 dp 表里面的"最大值"。

cpp 复制代码
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {

        int m = nums1.size(), n = nums2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        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(ret, dp[i][j]);
        return ret;
    }
};
相关推荐
S-X-S14 分钟前
算法解析-经典150(图论、回溯法)
算法·深度优先·图论
小冯的编程学习之路1 小时前
【LeetCode】:解数独【困难】
算法·leetcode·职场和发展
DC_BLOG1 小时前
数据结构排序
java·数据结构·算法·排序算法
G_qingxin1 小时前
前端排序算法
前端·算法·排序算法
大丈夫立于天地间1 小时前
OSPF - LSA对照表
网络·网络协议·学习·算法·信息与通信
Vec[95]3 小时前
如何将光源视角的深度贴图应用于摄像机视角的渲染
c++·算法·3d·贴图
23级二本计科3 小时前
排序算法的实现(插入,希尔,选择,冒泡,堆排,快排)
算法·排序算法
8Qi84 小时前
多目标优化算法——基于聚类的不规则Pareto前沿多目标优化自适应进化算法(CA-MOEA)
人工智能·算法·多目标优化·进化计算·群体智能·ca-moea
Swift社区4 小时前
LeetCode - #180 Swift 实现连续数字查询
算法·leetcode·swift