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

目录

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

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

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

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

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

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

题目七——712. 两个字符串的最小ASCII删除和 - 力扣(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]。


 代码如下:

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) 

 

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

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

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 存储结果。

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] 的 值。


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] 的顺序,其中 i 从 0 到 ms 的长度),j 从 0 到 np 的长度)。

5. 返回值

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


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]这两种情况任意一个满足,都是符合条件的,有的人可能就会像下面这样子写

                // 检查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(默认初始化值)

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

                // 检查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,你不就炸了吗? 

我们接着改进

 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,你不就炸了吗? 

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

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

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的所有字符。

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](注意这里 m 和 n 分别是 s1 和 s2 的长度,由于我们增加了额外的行和列,所以这里是 m+1 和 n+1),它表示两个字符串的最长公共子序列的ASCII最大和。

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

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


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-1 和 j-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 表里面的“最大值”。

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;
    }
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大模型与EDA工具

EDA工具&#xff0c;目标是硬件设计&#xff0c;而硬件设计&#xff0c;您也可以看成是一个编程过程。 大模型可以辅助软件编程&#xff0c;相信很多人都体验过了。但大都是针对高级语言的软件编程&#xff0c;比如&#xff1a;C&#xff0c;Java&#xff0c;Python&#xff0c…

单片机-串转并-74HC595芯片

1、74HC595芯片介绍 74HC595 是一个 8 位串行输入、并行输出的位移缓存器&#xff0c;其中并行输出为三态输出&#xff08;即高电平、低电平和高阻抗&#xff09;。 15 和 1 到 7 脚 QA--QH&#xff1a;并行数据输出 9 脚 QH 非&#xff1a;串行数据输出 10 脚 SCLK 非&#x…

融乐 1.1.6 | 拥有海量音乐资源的第三方音乐软件,支持无损音质下载

融乐Music是一款界面优美的第三方音乐软件&#xff0c;提供海量音乐资源。用户可以通过分类和搜索轻松找到想要的歌曲&#xff0c;并享受在线畅听和下载无损音质的功能。首页设有推荐歌单、精选和排行榜&#xff0c;帮助用户发现更多好音乐。此外&#xff0c;还支持设置歌词大小…

面试场景题系列:设计搜索自动补全系统

当我们在谷歌上搜索或者在亚马逊上购物时,只要在搜索框中打字,网页上就会展示一个或者更多的与搜索词匹配的结果。这个功能叫作自动补全(Autocomplete)、提前输入(Typeahead)、边输边搜(Search-as-you-type)或者增量搜索(Incremental Search)。图-1展示了一个谷歌搜索的示例,…

Leetcode打卡:设计一个ATM机器

执行结果&#xff1a;通过 题目 2241 设计一个ATM机器 一个 ATM 机器&#xff0c;存有 5 种面值的钞票&#xff1a;20 &#xff0c;50 &#xff0c;100 &#xff0c;200 和 500 美元。初始时&#xff0c;ATM 机是空的。用户可以用它存或者取任意数目的钱。 取款时&#xff0c…

【MySQL】九、表的内外连接

文章目录 前言Ⅰ. 内连接案例&#xff1a;显示SMITH的名字和部门名称 Ⅱ. 外连接1、左外连接案例&#xff1a;查询所有学生的成绩&#xff0c;如果这个学生没有成绩&#xff0c;也要将学生的个人信息显示出来 2、右外连接案例&#xff1a;对stu表和exam表联合查询&#xff0c;把…

在 IPhone 上检查 Safari 浏览历史记录的 5 种方法

与其他网络浏览器一样&#xff0c;Safari 会保留您的浏览历史记录&#xff0c;以便您可以输入之前访问过的网页。这是一个方便的功能。 但是如何在iPhone上查看已删除的浏览历史记录呢&#xff1f; 不用担心&#xff01;在本文中&#xff0c;我们将列出 5 个经过验证的选项&a…

使用Apache Mahout制作 推荐引擎

目录 创建工程 基本概念 关键概念 基于用户与基于项目的分析 计算相似度的方法 协同过滤 基于内容的过滤 混合方法 创建一个推荐引擎 图书评分数据集 加载数据 从文件加载数据 从数据库加载数据 内存数据库 协同过滤 基于用户的过滤 基于项目的过滤 添加自定…

SpringMVC(六)拦截器

目录 1.什么是拦截器 2.拦截器和过滤器有哪些区别 3.拦截器方法 4.单个拦截器的执行流程 5.使用拦截器实现用户登录权限验证&#xff08;实例&#xff09; 1.先在html目录下写一个login.html文件 2.在controller包下写一个LoginController文件 3.加拦截器 1.创建一个conf…

【FlutterDart】 拖动边界线改变列宽并且有边界高亮和鼠标效果(12 /100)

【Flutter&Dart】 拖动改变 widget 的窗口尺寸大小GestureDetector&#xff5e;简单实现&#xff08;10 /100&#xff09; 【Flutter&Dart】 拖动边界线改变列宽类似 vscode 那种拖动改变编辑框窗口大小&#xff08;11 /100&#xff09; 上效果 对比一下vscode的效果&…

4.1.2 栈和队列(一)

文章目录 栈的定义栈的基本运算栈的存储结构栈的应用表达式求值 栈和队列的逻辑结构与线性表相同&#xff0c;但是其运算受到限制&#xff0c;统称为运算受限的线性表。 栈&#xff0c; 先进后出 队列&#xff0c;先进先出 栈的定义 栈顶&#xff0c;唯一能操作端 栈底&#xf…

如何理解RDD,以及RDD的五大特性和五大特点。

RDD&#xff1a;英文全称Resilient Distributed Dataset&#xff0c;叫做弹性分布式数据集&#xff0c;代表一个不可变、可分区、里面的元素可并行计算的分布式的抽象的数据集合。 Resilient弹性&#xff1a;RDD的数据可以存储在内存或者磁盘当中&#xff0c;RDD的数据可以分区…

JavaWeb开发(六)XML介绍

1. XML介绍 1.1. 什么是XML &#xff08;1&#xff09;XML 指可扩展标记语言(EXtensible Markup Language)XML 是一种很像HTML的标记语言。   &#xff08;2&#xff09;XML 的设计宗旨是传输数据(目前主要是作为配置文件)&#xff0c;而不是显示数据。   &#xff08;3&a…

2000-2020年各省地区生产总值数据/各省gdp数据

2000-2020年各省地区生产总值数据/各省gdp数据 1、时间&#xff1a;2000-2020年 2、来源&#xff1a;国家统计局 3、指标&#xff1a;行政区划代码、地区、年份、地区生产总值 4、范围&#xff1a;31省 指标解释&#xff1a;地区生产总值&#xff08;Regional GDP&#xf…

鸿蒙HarmonyOS开发:基于Swiper组件和自定义指示器实现多图片进度条轮播功能

文章目录 一、概述1、场景介绍2、技术选型 二、实现方案1、图片区域实现2、底部导航点设计3、手动切换 三、所有代码1、设置沉浸式2、外层Tabs效果3、ImageSwiper组件 四、效果展示 一、概述 在短视频平台上&#xff0c;经常可以见到多图片合集。它的特点是&#xff1a;由多张…

第二十八周学习周报

目录 摘要Abstract1 GFPGAN1.1 总体结构1.2 实验研究1.3 代码分析 总结 摘要 本周主要的学习内容是GFPGAN模型。GFPGAN是一种基于生成对抗网络(GAN)的模型&#xff0c;其利用封装在预训练的人脸GAN中的丰富多样的先验进行人脸图像的修复。这种生成面部先验&#xff08;GFP&…

MCP(Model Context Protocol)模型上下文协议 进阶篇3 - 传输

MCP 目前定义了两种标准的客户端-服务端通信传输机制&#xff1a; stdio&#xff08;标准输入输出通信&#xff09;HTTP with Server-Sent Events (SSE)&#xff08;HTTP 服务端发送事件&#xff09; 客户端应尽可能支持 stdio。此外&#xff0c;客户端和服务端也可以以插件方…

NVIDIA DLI课程《NVIDIA NIM入门》——学习笔记

先看老师给的资料&#xff1a; NVIDIA NIM是 NVIDIA AI Enterprise 的一部分&#xff0c;是一套易于使用的预构建容器工具&#xff0c;目的是帮助企业客户在云、数据中心和工作站上安全、可靠地部署高性能的 AI 模型推理。这些预构建的容器支持从开源社区模型到 NVIDIA AI 基础…

物联网云平台:构建物联网生态的核心

我们常说的物联网&#xff0c;简称是IoT&#xff0c; 全称 Internet of Things。 用通俗的语言理解物联网&#xff0c;其实就是万事万物的互联网络。物联网概念也已经传播很多年了&#xff0c; 目前正在各行各业发挥力量。 要构建一个物联网生态&#xff0c; 我们首先想到的是智…

VS2022引入sqlite数据库交互

法一&#xff1a;用官网编译好的动态库(推荐) 下载所需文件 sqlite官网地址&#xff1a;https://www.sqlite.org/howtocompile.html 下载以下的2个压缩包 第一个压缩包 sqlite-amalgamation-xxxx.zip&#xff0c;xxxx是版本号,保持一致即可&#xff0c;这里面有sqite3.h 第…