【算法】【动规】双数组系列问题


跳转汇总链接

👉🔗动态规划算法汇总链接


4.1 最长公共子序列

🔗题目链接

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列
是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。 两个字符串的 公共子序列
是这两个字符串所共同拥有的子序列。

  1. 状态表示
    • dp[i][j] 表示,text1 的 [0, i] 区间和 text2 的 [0, j] 区间中,的最长公共子序列的长度
  2. 状态转移方程
    • 分析 i 和 j 位置,如果字符相等,那么其区间中的最长公共子序列一定可以以这个字符为结尾;如果不等,再分情况讨论

      dp[i][j] 的值有如下情况需要考虑:
      text1[i] == text2[j], dp[i-1][j-1] + 1;
      text1[i] != text2[j], 对如下值取 max:
      									dp[i-1][j]
      									dp[i][j-1]
      									dp[i-1][j-1] // 实际上这个情况已经被头两种包含了,可以不用写
      
  3. 初始化
    • 需要考虑 i-1、j-1 的越界情况,我们可以对空串进行设置,可以有效的防止越界
    • 这里在二维数组的前面多加一行和一列,分别表示两个数组为空串时候的 dp 值
      • 注意访问原数组时的对齐
    • 将空串也作为公共子序列,把多加的一行和一列都初始化为 0
  4. 填表顺序
    • 从上往下填写每一行,每行从左往右填写
  5. 返回值
    • dp[text1.size()][text2.size()],表的最右下角的值。

🐎代码如下:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n1 = text1.size() + 1;
        int n2 = text2.size() + 1;
        text1 = "-" + text1;
        text2 = "-" + text2;

        vector<vector<int>> dp(n1, vector<int>(n2));
        for(int i = 1; i < n1; i++)
            for(int j = 1; j < n2; 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[n1-1][n2-1];
    }
};

4.2 不相交的线

🔗题目链接

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

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直
线需要同时满足满足:
nums1[i] == nums2[j] 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

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

状态表示

  • dp[i][j] 表示,text1 的 [0, i] 区间和 text2 的 [0, j] 区间中,的最大连线数

略…(这一题和 4.1 根本是一样的,不交叉的连线不就是公共子序列嘛)

🐎代码如下:

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size() + 1;
        int n2 = nums2.size() + 1;
        vector<vector<int>> dp(n1, vector<int>(n2));
        for(int i = 1; i < n1; i++)
            for(int j = 1; j < n2; 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[n1-1][n2-1];
    }
};

4.3 不同的子序列(hard)

🔗题目链接

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

这一题 hard 在状态表示…

  1. 状态表示
    • dp[i][j] 表示,s 的 [0, i] 区间中有多少个子序列,满足和 t 的 [0, j] 区间的子串相同
  2. 状态转移方程
    • 分析区间内的 s 和 t 串,这里如果 s[i] 算作子序列的结尾 dp 是一种情况,如果 s[i] 不算作子序列的结尾 dp 又会是另一种情况,而最终这里的 dp 值应该是这两种情况相加。

      dp[i][j] 的值有如下情况需要考虑:
          s[i] 算作结尾,而且 s[i] == t[j], dp[i-1][j-1]
          s[i] 不算作结尾, dp[i-1][j]
      条件满足的情况下:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
      
  3. 初始化
    • 需要考虑 i-1、j-1 的越界情况,我们可以对空串进行设置,可以有效的防止越界
    • 这里在二维数组的前面多加一行和一列,分别表示两个数组为空串时候的 dp 值
      • 注意访问原数组时的对齐
    • 将空串也作为公共子序列,dp[i][0] 位置都置 1,意思是 t 为空串的时候 s 最少也包含这一个空串
    • 在 for 循环的时候初始化即可
  4. 填表顺序
    • 从上往下填写每一行,每行从左往右填写
  5. 返回值
    • dp[s.size()][t.size()],表的最右下角的值。

🐎代码如下:

class Solution {
public:
    int numDistinct(string s, string t) {
        int n1 = s.size() + 1;
        int n2 = t.size() + 1;
        vector<vector<double>> dp(n1, vector<double>(n2));
        dp[0][0] = 1;
        for(int i = 1; i < n1; i++)
        {
            dp[i][0] = 1;
            for(int j = 1; j < n2; j++)
                dp[i][j] = (s[i-1] == t[j-1]) ? 
                    dp[i-1][j] + dp[i-1][j-1] : 
                    dp[i-1][j];
        }
        return dp[n1-1][n2-1];
    }
};

4.4 通配符匹配(hard)

🔗题目链接

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

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

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

有了 4.3 的基础,状态表示就异曲同工,这题的关键点就是多种情况的考虑

  1. 状态表示
    • dp[i][j] 表示,s 的 [0, i] 区间的子串是否能被 p 的 [0, j] 区间的子串匹配
  2. 状态转移方程
    • 分析区间内的 s 和 p 串是否匹配,和 p 串最后一个位置的存放内容有关

      dp[i][j] 的值有如下情况需要考虑:
          p[j] 是普通字符, p[j] == s[i] && dp[i-1][j-1] == true, true
          p[j] == '?', dp[i-1][j-1] == true, true
          p[i] == '*', 匹配空串, dp[i][j-1] == true, true
          			 匹配一个字符, dp[i-1][j-1] == true, true
          			 匹配两个字符, dp[i-2][j-1] == true, true
          			 ...
          			 有一个为 true 即为 true
      
  • 按照上述为 ‘*’ 通配符进行匹配的方式需要一个循环遍历,这样的整时间复杂度是很大的(n3),所以我们需要想办法优化
  1. 优化
  • 针对 ‘*’ 号匹配符, 我们已知能推出的式子是这样的

    在 p[j] == '*' 的前提条件下,
    ①	dp[i][j] = dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || ...
    
  • 此时我们将 i 降一级,j 位置不变,所以仍是满足前提条件的,公式就可以写成如下:

    ②	dp[i-1][j] = dp[i-1][j-1] || dp[i-2][j-1] || ...
    
  • 将公式 ① 中的 dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || …,由公式 ② 中的 dp[i-1][j] 代替,可以得出化简后的表达式

    dp[i][j] = dp[i][j-1] || dp[i-1][j]
    
  1. 初始化
    • 按照惯例,这里涉及到空串,为了方便我们填表,在表头补充一行和一列
    • dp[0][0] 代表空串匹配空串,初始化为 true
    • dp[0][x] 代表 s 为空串而 p 不为空串,考虑到 p 如果以 ‘’ 开头 也是可以匹配的,这里就需要判断,当 p 从开始是 '’ 时为 true,遇到非 ‘*’ 的字符后,都是 false
    • dp[x][0] 代表 p 为空,全填 false
  2. 填表顺序
    • 从上往下填写每一行,每行从左往右填写
  3. 返回值
    • dp[s.size()][p.size()],表的最右下角的值。
class Solution {
public:
    bool isMatch(string s, string p) {
        int n1 = s.size() + 1;
        int n2 = p.size() + 1;
        s = '-' + s;
        p = '-' + p;
        vector<vector<bool>> dp(n1, vector<bool>(n2));
        // 初始化
        dp[0][0] = true;
        for(int j = 1; j < n2; j++)
            if(p[j] == '*') 
                dp[0][j] = true;
            else break;

        for(int i = 1; i < n1; i++)
            for(int j = 1; j < n2; j++)
            {
                if((s[i] == p[j] || p[j] == '?') && dp[i-1][j-1] == true)
                    dp[i][j] = true;
                if(p[j] == '*')
                    dp[i][j] = dp[i][j-1] || dp[i-1][j];
            }
        return dp[n1-1][n2-1];
    }
};

4.5 正则表达式匹配(hard)

🔗题目链接

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

  • ‘.’ 匹配任意单个字符
  • ‘*’ 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
其中,保证每次出现字符 * 时,前面都匹配到有效的字符

  1. 状态表示

    • dp[i][j] 表示,s 的 [0, i] 区间的子串是否能被 p 的 [0, j] 区间的子串匹配
  2. 状态转移方程

    • 分析区间内的 s 和 p 串是否匹配,和 p 串最后一个位置的存放内容有关

      dp[i][j] 的值有如下情况需要考虑:
          p[j] 是普通字符, p[j] == s[i] && dp[i-1][j-1] == true, true
          p[j] == '.', dp[i-1][j-1] == true, true
          p[j] == '*' 分如下情况:
          			p[j-1] == 普通字符,  匹配空串, dp[i][j-2]
          			p[j-1] == '.' , 匹配空串, dp[i][j-2]
          						    匹配不确定个字符, dp[i-1][j]
          			p[j-1] == s[i], , 匹配空串, dp[i][j-2]
          						    匹配不确定个字符, dp[i-1][j]
      
  3. 初始化

    • 按照惯例,这里涉及到空串,为了方便我们填表,在表头补充一行和一列
    • dp[0][0] 代表空串匹配空串,初始化为 true
    • dp[0][x] 代表 s 为空串而 p 不为空串,考虑到 p 如果以 “x*” 开头,并且只要偶数位置的 ‘x’ 连续出现就是可以匹配空串的,这些位置上设 true
    • dp[x][0] 代表 p 为空,全填 false
  4. 填表顺序

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

    • dp[s.size()][p.size()],表的最右下角的值。
class Solution {
public:
    bool isMatch(string s, string p) {
        s = '-' + s;
        p = '-' + p;
        int n1 = s.size();
        int n2 = p.size();
        vector<vector<bool>> dp(n1, vector<bool>(n2));
        // 初始化
        dp[0][0] = true;
        for(int j = 2; j < n2; j+=2)
            if(p[j] == '*')
                dp[0][j] = true;
            else
                break;

        for(int i = 1; i < n1; i++)
            for(int j = 1; j < n2; j++)
                if(p[j] == '*')
                    dp[i][j] = dp[i][j-2]                               // 匹配空串
                                || dp[i-1][j]                           
                                && (p[j-1] == '.' || p[j-1] == s[i]);   // 当 j-1 位置上是这两种情况时,匹配不确定个字符
                    // ps:&& 的优先级更高哦
                else 
                    dp[i][j] = (s[i] == p[j] || p[j] == '.') && dp[i-1][j-1];

        return dp[n1-1][n2-1];
    }
};

🥰如果本文对你有些帮助,欢迎👉 点赞 收藏 关注,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 若有差错恳请留言指正~~


  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值