当前位置: 首页 > news >正文

【动态规划】两个数组的 dp 问题一

两个数组的 dp 问题

  • 1.最长公共子序列
  • 2.不相交的线
  • 3.不同的子序列
  • 4.通配符匹配

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.最长公共子序列

题目链接:1143. 最长公共子序列

题目描述:

在这里插入图片描述

算法原理:

1.状态表示

这道题是遇到新的类型的题,两个字符串的dp问题。并且求的是最长公共子序列,我们很多题的经验就是从这道题来到。我们先总结一下这个经验。

经验 + 题目要求

  1. 选取第一个字符串 [0, i] 区间以及第二个字符串 [0, j] 区间作为研究对象
  2. 根据题目要求,确定状态表示

先以这两段区间来研究,这道题研究的是最长公共子序列。也就是说先看看上面s1这个区间所有子序列,以及下面s2这个区间所有子序列,其中的最长子序列。

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

在这里插入图片描述
如果以某个位置为结尾所有子序列,还有在去找子序列,时间复杂O(N^4),而我们这里的子序列是所有的子序列,比如是说 [0,i] 区间所有的子序列。这个子序列既包含以i为结尾,也包含不以i为结尾的。

2.状态转移方程

根据最后一个位置的情况,分情况讨论

如果最后一个位置字符相等 s[i] == s[j]

是不是可以先在s1 [ 0 , i-1] 和 s2 [0 , j - 1]区间内所有子序列找最长公共子序列长度 然后 加上 1就行了

在这里插入图片描述

如果最后一个位置字符相等 s[i] != s[j]

那么最长公共子序列一定不是以 i j 位置为结尾,也就是说最长公共子序列一定不是同时选择 i 和 j 位置。那可以这样选

  1. s1[0, i - 1],s2[0, j] ,不选 i
  2. s1[0, i],s2[0, j -1] ,不选 j
  3. s1[0, i - 1],s2[0, j - 1], i j都不选

在这里插入图片描述
我们一般找状态转移方程要把所有情况不重不漏都找完,情况1 和 情况2 都包含情况3。但是注意我们的状态表示是最长公共子序列的长度,我们要找的是多种情况的max。所有这里的重复不会影响。但是如果要找的是公共子序列的个数,那就有问题了。遇到了再说。还有情况1和情况2包括情况3,所以填表的时候情况3就不用考虑了。

在这里插入图片描述

3.初始化

关于字符串的 dp 问题: 空串是有研究意义的

比如 s1 选一个空串,s2 选一个空串,其实也是一个公共子序列,不过公共子序列长度为0。

引入空串的概念之后,会方便我们的初始化

s1 [0,i] 区间,i最小0,最差就是 [0,0]是一定有一个字符的,如何表示空串呢?可以在原始 dp 表上多来一行一列,第一行表示第一个字符串为空,第二行表示第二个字符串为空。

并且填表也不会越界了。而且第一行表示第一个字符串为空,所以最长公共子序为0,第一列表示第二个字符串为空,所以最长公共子序也是0。

在这里插入图片描述

引入辅助节点要注意两个注意事项:

  1. 里面的值要保证我们后序填表是正确的
  2. 下标的映射关系

因为多开一行一列相当于dp表向右下移动一位,如果要回原表肯定是要 i-1,j-1。但是在字符串这里,我们可以在每个字符串前面加一个 ‘ ’,然后字符串有效字符就从1开始计数了。这个时候填表 1,1位置的时候就是对应字符串1,1位置。

4.填表顺序

填 i j 会遇到左上,上,左,因此从上往下填写每一行,每一行从左往右
在这里插入图片描述

5.返回值

dp[i][j] 表示:s1 的 [0, i] 区间以及 s2 的[0, j] 区间内所有子序列中,最长公共子序列的长度,而我们要的是s1,s2整个字符串的最长子序列的长度。因此 返回 dp[m][n]

class Solution {
public:int longestCommonSubsequence(string s1, string s2) {// 1.创建 dp 表// 2.初始化// 3.填表// 4.返回值int m = s1.size(), n = s2.size();s1 = ' ' + s1, s2 = ' ' + s2;// 处理下标映射关系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])if(s1[i] == s2[j])dp[i][j] = dp[i - 1][j - 1] + 1;elsedp[i][j] = max(dp[i - 1][j],dp[i][j - 1]); // 返回结果return dp[m][n];}
};

2.不相交的线

题目链接: 1035. 不相交的线

题目分析:

在这里插入图片描述

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

可以以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

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

在这里插入图片描述

先分析一下这道题,下面的最终结果,发现上面数组选252,下面数组也选252。这两个是一样的。是不是相当于在第一个数组中选一个子序列,然后在第二个数组里面也选一个子序列。然后选的这两个子序列要求必须得一样,且最长。那么正好是这两个数组里面得最长公共子序列。
在这里插入图片描述

由于这道题和上面几乎一模一样,一些细节就不在细说了

算法原理:

1.状态表示

经验 + 题目要求

  1. 选取第一个字符串 [0, i] 区间以及第二个字符串 [0, j] 区间作为研究对象
  2. 根据题目要求,确定状态表示

dp[i][j] 表示 :在n1里面的 [0,i] 区间以及 n2里面的 [0,j] 区间里面的所有子序列中,最长公共子序列的长度

在这里插入图片描述

2.状态转移方程

在这里插入图片描述

3.初始化

多开一行一列,填表就不会越界了。

  1. 里面的值要保证后序填表是正确的
  2. 下标的映射关系

第一行表示第一个数组里面是没有元素的。第一列表示第二个数组里面没有元素。因此给0。

整体往右下,填表要注意下标映射关系。
在这里插入图片描述

4,填表顺序

在这里插入图片描述

5.返回值

返回dp[m][n]

class Solution {
public:int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {// 1.创建 dp 表// 2.初始化// 3.填表// 4.返回值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;elsedp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);return dp[m][n];}
};

3.不同的子序列

题目链接:115. 不同的子序列

题目分析:

在这里插入图片描述

返回s 的 子序列 中 t 出现的个数

在这里插入图片描述

算法原理:

1.状态表示

经验 + 题目要求

  1. 选取第一个字符串 [0, i] 区间以及第二个字符串 [0, j] 区间作为研究对象
  2. 根据题目要求,确定状态表示

先不考虑整个字符串,先考虑 t [0, i] 区间,以及 s [0, j] 区间,先研究这两个小区间。我们题目要求在s的子序列里有多少个t,如果选这两个子区间的话相当于在 [0, j]这个区间里面的子序列里找一下有多少个 [0,i]子串。

在这里插入图片描述

所以我们的状态表示:

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

在这里插入图片描述

2.状态转移方程

我们是在s的 [0 ,j]子序列中找找有多少t [0,i] 子串。其实可以把 s 中 [0 ,j] 所有子序列划分成两类。

根据 s 的子序列的最后一个位置包不包含 s[j] 划分两类:

包含s[j]。子序列最后一个位置是j,我们要找子序列有多少t,那子序列要想有t,说明 t 最后一个位置和 j 是相等的。t[i] == s[j],要求dp[i][j],既然最后一个位置t[i] == s[j],可以先把它们去掉,然后在 t [0, i -1] 和 s [0 , j-1]研究问题,也就是说在 s [0 , j-1]里所有子序列中有多少个t [0, i -1] 子串。如果找到有多少个然后后面填上t[i] 和 s[j] 就行了。那有多少个呢?正好是 dp[i - 1][ j - 1],后面不加1。

在这里插入图片描述

不包含s[j],那就在 s [0, j -1] 所有子序列中找 t [0, i] 子串。

在这里插入图片描述

最后dp[i][j] 是这两种情况加起来

在这里插入图片描述

3.初始化

  1. 引入空串
  2. 里面的值要保证后序填表是正确的
  3. 下标的映射关系

引入空串就是在原始的dp中上面多加一行,左边多加一列。
第一行表示第一个字符串为空
第一列表示第二个字符串为空
这样填表就不会发生越界了

在这里插入图片描述

接下来看里面的值应该填多少

在字符串里,可以根据空串的性质初始化这些位置。

第一行表示 t 字符串为空串,t 如果是空串,s 不管是多长子序列里面都有一个空串。因此第一行应该全都是1

在这里插入图片描述

第一列表示 s 字符串为空串,只有当 t 是一个空串,才有一个子序列。 当 t 不是空串,怎么在 s 中也找不到一个子序列是 t。所有除了第一个位置外其他都是0。

在这里插入图片描述

下标映射关系。有两种方法

  1. 每个字符串前面加一个任意字符 s = ‘ ’ + s
  2. 整体往右下移动一步,回原本要-1

4.填表顺序

从上往下每一行,每一行从左往右

5.返回值

dp[i][j] 表示:s字符串 [0, j] 区间内所有子序列中,有多少个 t 字符串 [0, i] 区间内的子串。题目要求返回在 s 的 子序列 中 t 出现的个数。

在这里插入图片描述

class Solution {
public:int numDistinct(string s, string t) {// 1.创建 dp 表// 2.初始化// 3.填表// 4.返回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];}
};

4.通配符匹配

题目链接:44. 通配符匹配

题目描述:

在这里插入图片描述

算法原理:

1.状态表示

经验 + 题目要求

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

在这里插入图片描述

2.状态转移方程

根据最后一个位置的状况,分情况讨论

p[j]是普通字符,如果 i 位置 和 j 位置字符相等,并且 s [0, i - 1]能被 p[0, j -1] 匹配上,那 [0, i] 就能被 [0, j] 匹配上

在这里插入图片描述

p[j] == ‘?’ ,'?'可以匹配任何单个字符,因此只用去看s [0, i - 1]能否被 p[0, j -1] 匹配上

在这里插入图片描述

p[j] == ’ * ',( ’ * ’ 可以匹配任意字符序列(包括空字符序列)。)
如果p[j]匹配空串,我们就看dp[i][j-1]

在这里插入图片描述

如果p[j]匹配1个字符,我们就看dp[i-1][j-1]

在这里插入图片描述

如果p[j]匹配2个字符,我们就看dp[i-2][j-1]

在这里插入图片描述

dp[i][j] 时间复杂度就是O(N^2),在加上p[j] == ’ * ',时间复杂度就是O(N^3)了。想个办法看看p[j] == ’ * ',能否由若干个有限的状态表示。

优化:
第一种方法:数学

p[j] == ‘*’,有这么多种情况,只要有一种情况为真就可以了,所以我们可以得到下面的等式。发现 j -1是不变的,让等式所有 i 都减1。然后就可以进行替换了。

在这里插入图片描述

第二种方法:根据状态表示以及实际情况,优化状态转移方程

p[j] == ‘*’,找到dp[i][j]。

当j位置是 ‘*’,第一种情况还是去匹配空串。
在这里插入图片描述

第二种情况,去匹配一个字符 ,但是匹配完不把 j 位置丢掉,继续去看 s[0, i -1] 能否被 p[0, j]匹配

在这里插入图片描述
可能会有疑问,为什么’ * ‘就匹配一个,明明可以匹配多个,并且匹配完为什么不把’ * '丢掉。

我们这里是对 p[j] == ’ * ’ 好多情况的优化,我们让p[j] == ’ * ‘只匹配一个,但不舍去得到的 dp[i-1][j],也就是说在dp[i-1][j] 这一个状态里面,j位置 依旧是’ * ',它依旧有两种情况,要么去匹配空串,要么i-1匹配上,然后去dp[i-2][j]考虑匹配2个字符的情况。同理在dp[i-3][j]考虑匹配3个字符的情况。也就是说这个 j 会向下传递直到把s[0,i]所有字符匹配。

也就是说当匹配一个,不丢弃的话,进入dp[i-1][j],’ * ’ 继续会把 i -1匹配掉。同理进入dp[i-2][j],’ * ’ 继续会把 i -2匹配掉。。。所有仅需匹配一个不丢弃就可以把所有情况都考虑掉。

在这里插入图片描述

3.初始化

  1. 引入空串
  2. 里面的值要保证后序的填表是正确的
  3. 下标的映射关系

dp表上面多加一行表示s是空串,左边多加一列表示p是空串。接下来看看里面值应该填什么。

在这里插入图片描述
当 s 是空串,p也是空串,肯定能匹配上,所以第一行第一个格子是true

在这里插入图片描述
当s 是空串,但是 p 不是空串的话,注意 ’ * ‘ 可以匹配空串。现在就要对p的字符串就行讨论,然后初始化第一行后面的位置,如果s是空字符串,p字符串 前面出现’ * ‘ 可以匹配空串,但是 ’ * ‘ 之后出现普通字符了,后面不管有多少个’ * '都不能匹配了。
在这里插入图片描述
现在考虑第一列,如果p是空串,s也是空串,肯定能匹配,所以第一列第一个空格还是true。

后续如果 p 是空串,s不是空串,肯定匹配不上,所以第一列后续都是false

在这里插入图片描述

下标映射有两种方法:

  1. dp表多加一行一列整体向右下移动一位,如果要回原表需要 i -1,j -1 才行
  2. s = ’ ‘ + s,字符串前面加一个辅助字符,这样字符串字符就和dp表一一对应了。

4.填表顺序

从上往下填写每一行,每一行从左往右

在这里插入图片描述

5.返回值

dp[i][j] 表示:p[0, j] 区间内的子串能否匹配 s[0, i] 区间内的子串。题目要求是整个字符串,因此返回dp[m][n],m是s的长度,n是p的长度

class Solution {
public:bool isMatch(string s, string p) {// 1.创建 dp 表// 2.初始化// 3.填表// 4.返回值int m = s.size(), n = p.size();vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));//初始化dp[0][0] = true;for(int j = 1; j <= n; ++j)if(p[j - 1] == '*') dp[0][j] = true;else break;for(int i = 1; i <= m; ++i)for(int j = 1; j <= n; ++j)if((s[i - 1] == p[j - 1] || p[j - 1] == '?') && dp[i - 1][j -  1])dp[i][j] = true;else if(p[j - 1] == '*')dp[i][j] = dp[i][j - 1] || dp[i - 1][j];return dp[m][n];}
};

http://www.mrgr.cn/news/30768.html

相关文章:

  • 软考高级:数据库规范化: 1NF、2NF、3NF和 BCNF AI 解读
  • Google Gemini 与 OpenAI 激烈竞赛:语音 AI 与未来智能体的技术演进
  • 基于Tesseract_OCR识别
  • 透明LED模块的应用场景
  • 简单题70.爬楼梯 (Java)2024920
  • Axure PR 9 步进器 设计交互
  • 国际知名度最高的华人改名大师颜廷利:当代最牛的易经姓名学泰斗
  • Spring 的循环依赖
  • .NET 一直跻身 30 大Github最活跃开源项目之列。
  • 【每天学点AI】一个例子带你了解Python装饰器到底在干嘛!
  • MySQL_简介及安装、配置、卸载(超详细)
  • pig4cloud中RequestMatcher的添加
  • Python知识点:详细讲解在Python编程中,GIL(全局解释器锁)的影响与规避方法
  • Vue子组件样式受到父组件污染
  • 计算机组成原理之计算机硬件的基本组成
  • 会计稳健性Cscore模型(2000-2022年)
  • 深入探索NumPy
  • 等保测评:企业如何构建安全的网络架构
  • LIN总线CAPL函数—— 设置与测量从节点的波特率(linSetRespBaudrate)
  • 使用JavaWeb开发注册功能时,校验用户名是否已存在的一个思路(附代码)