当前位置:   article > 正文

算法沉淀——动态规划之两个数组的 dp(上)(leetcode真题剖析)_动态规划dp数组

动态规划dp数组

在这里插入图片描述

算法沉淀——动态规划之两个数组的 dp

01.最长公共子序列

题目链接:https://leetcode.cn/problems/longest-common-subsequence/

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

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

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。
  • 1
  • 2
  • 3

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
  • 1
  • 2
  • 3

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。 
  • 1
  • 2
  • 3

提示:

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

思路

状态转移方程:

  1. 如果当前字符相同(text1[i] == text2[j]),说明我们找到了一个新的公共字符,因此最长公共子序列的长度在之前的基础上加 1:
    • dp[i][j] = dp[i - 1][j - 1] + 1
  2. 如果当前字符不同(text1[i] != text2[j]),则我们需要考虑去掉 text1[i] 或者去掉 text2[j] 之后的最长公共子序列。这可以通过比较 text1 的前 i-1 个字符和 text2 的前 j 个字符的最长公共子序列长度,以及 text1 的前 i 个字符和 text2 的前 j-1 个字符的最长公共子序列长度,取最大值:
    • dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

初始化:为了方便处理边界情况,我们在文本 text1text2 前各添加一个空字符,即 text1[0]text2[0] 表示空串。这样,dp 表的大小为 (m+1) x (n+1),其中 mn 分别是两个文本的长度。初始化时,将第一行和第一列的值都设置为 0。

填表顺序:最终的结果存储在 dp[m][n] 中,其中 mn 是两个文本的长度。这个值表示 text1text2 的最长公共子序列的长度。

代码

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m=text1.size(),n=text2.size();

        text1=" "+text1,text2=" "+text2;
        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(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]);

        return dp[m][n];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

02.不相交的线

题目链接:https://leetcode.cn/problems/uncrossed-lines/

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 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 的直线相交。
  • 1
  • 2
  • 3
  • 4

示例 2:

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

示例 3:

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

提示:

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

思路

如果我们希望确保两条直线不相交,我们可以将问题转化为在两个整数数组中寻找最长的公共子序列。这是因为,如果两条直线不相交,它们在同一列上对应的两个元素在两个数组中的顺序关系应该保持一致。

这个问题可以用动态规划来解决,具体步骤如下:

  1. 状态表达
    • 我们选取两个数组分别表示两条直线,数组长度分别为 mn
    • 定义状态表 dp,其中 dp[i][j] 表示数组1的前 i 个元素和数组2的前 j 个元素中的最长公共子序列的长度。
  2. 状态转移方程
    • 如果 nums1[i] == nums2[j],说明找到了一个公共元素,状态转移方程为 dp[i][j] = dp[i-1][j-1] + 1
    • 如果 nums1[i] != nums2[j],则需要在两个数组的前面部分分别寻找最长公共子序列,状态转移方程为 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  3. 初始化
    • 为了方便处理边界情况,可以在两个数组的前面各添加一个虚拟元素,表示空串。
    • 初始化 dp 数组的第一行和第一列为0,因为空串与任何数组的最长公共子序列长度都为0。
  4. 填表顺序
    • 从上到下,从左到右填写 dp 数组。
  5. 返回值
    • 最终的结果存储在 dp[m][n] 中,表示两个数组的最长公共子序列的长度。

代码

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];
    }   
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

03.不同的子序列

题目链接:https://leetcode.cn/problems/distinct-subsequences/

你两个字符串 st ,统计并返回在 s子序列t 出现的个数,结果需要对 109 + 7 取模。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

提示:

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

思路

  1. 状态表达

    • 选取第一个字符串 [0, i] 区间以及第二个字符串 [0, j] 区间作为研究对象,结合题目要求来定义状态表达。
    • 在这道题中,状态表达为 dp[i][j],表示在字符串 s[0, j] 区间内的所有子序列中,有多少个 t 字符串 [0, i] 区间内的子串。
  2. 状态转移方程

    • 根据最后一个位置的元素,结合题目要求进行分类讨论:

      • t[i] == s[j]
        
        • 1

        时:

        • 选择 s[j] 作为结尾,相当于在状态 dp[i-1][j-1] 中的所有符合要求的子序列的后面,再加上一个字符 s[j],此时 dp[i][j] = dp[i-1][j-1]
        • 不选择 s[j] 作为结尾,相当于选择了状态 dp[i][j-1] 中所有符合要求的子序列,继承了上个状态里面的求得的子序列,此时 dp[i][j] = dp[i][j-1]
      • t[i] != s[j]
        
        • 1

        时:

        • 子序列只能从 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]
  3. 初始化

    • 引入空串后,方便初始化,注意下标的映射关系以及确保后续填表是正确的。
    • s 为空时,t 的子串中有一个空串和它一样,因此初始化第一行全部为 1
  4. 填表顺序

    • 从上往下填每一行,每一行从左往右。
  5. 返回值

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

代码

class Solution {
public:
    int numDistinct(string s, string t) {
        int m=t.size(),n=s.size();
        vector<vector<double>> dp(m+1,vector<double>(n+1));
        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];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

04.通配符匹配

题目链接:https://leetcode.cn/problems/wildcard-matching/

给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 '?''*' 匹配规则的通配符匹配:

  • '?' 可以匹配任何单个字符。
  • '*' 可以匹配任意字符序列(包括空字符序列)。

判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
  • 1
  • 2
  • 3

示例 2:

输入:s = "aa", p = "*"
输出:true
解释:'*' 可以匹配任意字符串。
  • 1
  • 2
  • 3

示例 3:

输入:s = "cb", p = "?a"
输出:false
解释:'?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
  • 1
  • 2
  • 3

提示:

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

思路

在处理两个字符串之间的动态规划问题时,通常按照以下步骤进行:

  1. 状态表达

    • 选取第一个字符串 [0, i] 区间以及第二个字符串 [0, j] 区间作为研究对象,结合题目的要求定义状态表达。
    • 在这道题中,我们定义状态表达为 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][j-1]
      • p[j] == '*' 时,匹配策略有两种选择:
        • 选择 '*' 匹配空字符串,相当于它匹配了一个寂寞,直接继承状态 dp[i][j-1]dp[i][j] = dp[i][j-1]
        • 选择 '*' 向前匹配 1 ~ n 个字符,直至匹配整个 s 串。相当于从 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-1][j-1]
        • p[j] == '*' 时,状态转移方程优化为:dp[i][j] = dp[i-1][j] || dp[i][j-1]
  3. 初始化

    • dp 数组的值表示是否匹配,初始化整个数组为 false
    • 由于需要用到前一行和前一列的状态,初始化第一行和第一列。
      • dp[0][0] 表示两个空串是否匹配,初始化为 true
      • 第一行表示 s 为空串,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));
        dp[0][0]=true;
        for(int i=1;i<=n;i++)
            if(p[i]=='*') dp[0][i]=true;
            else break;

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

        return dp[m][n];
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/556804
推荐阅读
相关标签
  

闽ICP备14008679号