0. 动态规划
介绍本篇之前,我想先用人话叙述一般解决动态规划问题的思路:
动态规划的问题,本身有许多产生结果的可能,需要在具体题目下得到满足某个条件的解。
如何得到呢?
我们就需要根据这个具体问题,建立一个状态表(dp 表),在这张 dp 表中的每一个位置的数据都有明确的、相同的定义,称为 状态表示,而储存的每个数据,就是由原问题产生的每一个子问题的解。
子问题的解又该怎么得到呢?
这就需要通过对子问题如何推导(状态的转移过程)进行具体分析,要求是每个解都能由前一个解得到,从而得到所有的子问题的解。根据得到的解把 dp 表填满,最后就能在这张 dp 表中找到我们要的具体答案。
梳理一下:
🎯五个思考步骤 和 注意事项
-
状态表示:自定义一下 dp 表里的每个数据(子问题的解)有什么统一的含义,就是 dp[i] 是什么
- dp 表可能是一维数组、二维数组…需要具体分析进行选择
- 这里的含义需要我们根据题目和经验得出,这里就给大家一个分享一个经验,你可以试着把 i 位置定义成 “以 i 位置为结尾 / 为开头的 xxxx”
- 状态转移方程:一个通用公式,可以用 最近一步 已知状态推出下一个状态,最终达到填满 dp 表的所有状态(即得到所有子问题的解)
- 初始化:保证填表时不越界(根据状态转移方程看哪里有越界的可能,再决定怎么初始化)
- 填表顺序:为了填表时,所需要的状态是已知的
- 返回值:题目要求 + 状态表示
🎯技巧
- 如果 dp[i] 有例如 “选 / 不选” 两种或多种状态,那么建立两个或多个dp表,也可以把几个表写成二维数组
- 巧妙利用数组元素和数组下标(尤其是对于 “无序 + 重复” 数据可用)
- eg:下标对应原数据,元素对应出现个数 / 总合…(见题 1.5)
- 画出每种状态的转移方程(状态机),对照图去写 dp 转移方程会更加清楚,不容易出错。
- 初始化:
- 多加一位数 / 一行 / 一列
-
此种情况需要注意对原数组访问的时候下标还需要多减一位
-
如果原数组是字符串,可以对原数组加一个虚拟位置(见题1.4),如下
str = '-' + str;
-
- 为越界的位置增加一个 if() 判断,会稍微改变一点 dp 转移方程的结构
- 对于用最大或最小值初始化时会有一个计算后溢出的问题,强烈建议使用这个数据
±0x3f3f3f3f
(int 最大值的一半)
- 多加一位数 / 一行 / 一列
- 对于 “环形xx” 想办法转成普通线性关系会简单很多
优化思路
- 滚动数组,只留下所需状态求下一个状态,滚动进行。需要注意的是,滚动复制的顺序不能被覆盖。
ps:此种优化对于算法本身不是很重要,可以作为了解。 - 涉及到类似字典搜索,考虑插入后用 hash 表搜索。(见题 1.4)
1. 子数组系列
分析状态时的通用方法:
- 分成 自身、自身+之前一个元素 进行具体考虑
- 如果题目和数字有关,分成 >0、<0 进行考虑
1.1 乘积为正数的最长子数组长度
🔗详解点击
给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
1.2 等差数列划分
🔗详解点击
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的子数组个数。
子数组是数组中的一个连续序列。
1.3 最长湍流子数组
🔗详解点击
给定一个整数数组 arr ,返回 arr 的最大湍流子数组的长度 。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组 。
ps: 就是元素两两之间的增长情况是 ↗↘↗↘↗.... 这样
1.4 单词拆分
🔗详解点击
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。
请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
1.5 环绕字符串中的子字符串
🔗详解点击
定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串,
所以 base 看起来是这样的:
"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".
给你一个字符串 s ,请你统计并返回 s 中有多少 不同非空子串 也在 base 中出现。
2. 子序列系列
需要和子数组区别,子序列两个要求是:不改变原序列顺序,不要求连续取数
分析状态时的通用方法:
- 分成 自身、自身+之前一个元素 进行具体考虑;
- 由于取数不连续,除了分析 dp[] 表中 i 位置,还需要一个 j 位置,确定子序列结尾的前一个元素。时间复杂度是 n2。
- dp[i][j] 这样的二维数组,可以表示子序列倒数两个数的位置 (i, j),用到了二维数组的上三角(i 为行,j 为列,不包括对角线)
2.1 最长递增子序列
🔗详解点击
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而 不改变其余元素的顺序。
例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
2.2 摆动序列
🔗详解点击
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。
第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
2.3 最长递增子序列的个数
🔗详解点击
给定一个未排序的整数数组 nums,返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
2.4 最长数对链
🔗题目连接
tips:sort() 排序
ps:经典课表安排问题,前面看过了就知道这道很简单,类似的题解有很多,欢迎点击链接直接刷题
2.5 最长定差子序列
🔗详解点击
给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。
子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。
ps:题目本身不难,突出一个优化手段
2.6 最长的斐波那契子序列的长度
🔗详解点击
如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:
n >= 3
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。
如果一个不存在,返回 0 。
2.7 最长等差数列(hard)
🔗详解点击
给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。
回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], ..., nums[ik] ,且 0 <= i1 < i2 < ... < ik <= nums.length - 1。
并且如果 seq[i+1] - seq[i]( 0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。
2.8 等差序列划分Ⅱ(hard)
🔗详解点击
这一题已经跟随上一题完整的分析过一遍了,放一个 leetcode 题解和代码的链接,不懂的看 2.6+2.7。
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差
3. 回文串系列
📕子串问题:
子串和子数组基本一致,要求连续,但是即使回文串内容相同,但是如果在原数组中的首尾位置不同,也算作不同的回文串。
分析回文串问题,通常可以看到三种方法,对于 3.1 题目来分析一下这三种方法:
- 中心扩展算法(后面更新):时间复杂度(n2),空间复杂度(1)
- 马拉车算法():时间、空间复杂度都是(n),但是只局限使用在回文串题目中,感兴趣的可以自己了解一下
- 动态规划算法:时间、空间复杂度都是(n2),我们这里选择动态规划的需求很明确,一是了解思想,二是可以将后续一些hard级别的回文串题目直接拉到easy等级(笑)。利用的就是一张dp表,能够将所有的字串是否是回文的信息,保存在dp表里面。
给一个比较通用的状态表示方式:
- dp[i][j] 表示,以 i 为开头 j 为结尾的子串,是否为回文串。
📕子序列问题:
回文串 + 子序列 问题的变式,这里再次提醒:注意状态表示,和 2.x 中子序列又是有区别的。2.x 中子序列多是能由末尾两个推导出来所有子序列所以定义 dp[i][j] 为以 i、j 结尾的子序列,回文串子序列并没有固定的推导规则,这里的状态表示如下:
- dp[i][j] 表示,以 i 为开头 j 为结尾的子序列,xxxxx。
👇回文串系列集合度高,适合一起看,一个系列的题都在这一个链接里
🔗详解点击
3.1 回文子串
给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
3.2 最长回文子串
解法与上题类似
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
3.3 分割回文串IV(hard)
标记是 hard,但是用动规预处理一下数据,生成dp表,在分析就变得很简单了。
给你一个字符串 s ,如果可以将它分割成三个 非空 回文子字符串,那么返回 true ,否则返回 false 。
3.4 分割回文串II(hard)
首先,预处理数据,方便判断子串是否是回文串;
接着,剩下的部分与 1.4 单词拆分题 分析方法一样。
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
3.5 最长回文子序列
注意状态表示
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
3.6 让字符串成为回文串的最少插入次数(hard)
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
4. 双数组系列
通常来说,有几个点需要注意:
- 选取第一个字符串的 [0, i] 区间和第二个字符串的 [0, j] 区间作为研究对象,根据题目要求确定状态表示。(注意不是以 i 或 j 为结尾)
- 空串在有时候是有研究意义的,同时,空串还能方便我们初始化
ps:为了让主页好看一点,博主后续会把一个系列的题解放一块啦~
👉🔗双数组系列题解链接
4.1 最长公共子序列
🔗题目链接
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
4.2 不相交的线
🔗题目链接
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j] 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
4.3 不同的子序列(hard)
🔗题目链接
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 10^9^ + 7 取模。
4.4 通配符匹配(hard)
🔗题目链接文章来源:https://www.toymoban.com/news/detail-766145.html
给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 '?' 和 '*' 匹配规则的通配符匹配:
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
4.5 正则表达式匹配(hard)
🔗题目链接文章来源地址https://www.toymoban.com/news/detail-766145.html
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
其中,保证每次出现字符 * 时,前面都匹配到有效的字符
到了这里,关于【算法】动态规划(dp问题),持续更新的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!