动态规划
不连续子序列问题
[300] 最长递增子序列
题目
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
解法
只有一个字符串 => 一维dp数组
求递增子序列 => 判断条件即两个树的大小比较
子序列 => 不需要连续,因此要固定一个位置然后再开循环遍历,双层
动态规划五部曲
- 确定dp数组的含义
dp[i]表示以i为结尾且包括nums[i]的最长严格递增子序列的最长长度
- 确定状态转移方程
若当前元素nums[j]大于nums[i],那么以nums[i]为结尾的子序列就可以加上nums[j],所以长度应该加上1;而如果不大于那么就保持原值即可。我们需要遍历每个小于j的i的值,比较并更新。
1 | if (nums[j] > nums[i]) dp[j] = max(dp[j], dp[i] + 1); |
- 如何初始化dp数组
对于每个dp[j]最初都至少包含了自己,即1
- 如何遍历
外层循环固定j表示每一个位置,然后内层循环遍历i,i应该是从0到j-1以此来更新以nums[j]为结尾的子序列的长度
- 拿个例子推导一下
代码
1 | class Solution { |
[1143] 最长公共子序列
题目
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
解法
- 确定dp数组的含义
dp[i][j]
表示对于text1的0到i-1的子串和text2的0到j-1的子串的最长公共子序列的长度
- 确定状态转移方程
公共 => 判断两个数字是否相同。如果相同,就是上一个dp[i - 1][j - 1]
的公共长度加上1
=> 不相同,继承上一个状态的最大值,上一个状态是0到i-2和0到j-1(左边一格)的公共子序列长度 以及 0到i-1和0到j-2(右边一格)的公共子序列长度 中的较大值
1 | if (text1[i - 1] == text2[j - 1]) { |
- 如何初始化dp数组
0下标表示空串,text1和空串公共子序列长度为0,text2同理,所以初始化全为0。
- 如何遍历
从小到大遍历
代码
1 | class Solution { |
[1035] 不相交的线
题目
在两条独立的水平线上按给定的顺序写下 nums1
和 nums2
中的整数。
现在,可以绘制一些连接两个数字 nums1[i]
和 nums2[j]
的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
解法
上一道题目(1143)套皮,本质是一道题
连续子序列问题
[674] 最长连续递增序列
题目
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
解法
- dp数组的含义
dp[i]表示以i结尾下标包含nums[i]连续递增子序列的最长长度
- 状态转移方程
递增 => 判断后面是否大于前面
连续 => 不用固定再遍历,只需要简单比较前一位即可,因此只需要一层循环
1 | if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1; |
- dp数组如何初始化
最短都是自己,因此都初始化为1
- 如何遍历
依赖前一个,从前往后遍历
- 推导例子
代码
1 | class Solution { |
[718] 最长重复子数组
题目
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
解法
- dp数组的含义
dp[i][j]
表示nums1[i - 1]和nums2[j - 1]的最长公共子数组
- 状态转移方程
公共 => 判断两个元素是否相等,不相等则为0,因为要是连续的
1 | if (nums1[i - 1] == nums[j - 1]) |
- dp数组如何初始化
下标0表示空数组,所以dp[i][0]
= 0, dp[0][j]
= 0
- 如何遍历
从前往后双层遍历,都从1开始,需要一个res来保存
- 推导例子
代码
1 | class Solution { |
[53] 最大子序和
题目
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
解法
- dp数组的含义
dp[i]表示以i为结尾包含nums[i]元素的连续子数组的最大和
- 状态转移方程
- dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
- nums[i],即:从头开始计算当前连续子序列和(类比最长连续递增取1,但是因为本身就是1所以判断完不用写else语句)
一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
- 初始化 => dp[i]初始化为nums[i](类比那个最长连续递增初始化为1),但是我们可以用递推公式免去除了0以外的初始化
- 遍历顺序 => 从小到大遍历
- 推导一遍例子
代码
1 | class Solution { |
距离问题
[392] 判断子序列
题目
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"
是"abcde"
的一个子序列,而"aec"
不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
解法
- 确认dp数组含义
s是t的子序列,dp[i][j]
代表s[i - 1]是否是t[j - 1]的子序列
- 状态转移方程
判断当前s[i - 1]是否和t[j - 1]相等,如果相等且dp[i - 1][j - 1] == true
,则dp[i][j]
也为true,所以就是如果相等的话dp[i][j]
的值取决于dp[i - 1][j - 1]
如果不相等那么当前字符这个t[j - 1]可能就是我们需要删去的字符,是否为子序列就需要看s[i - 1]和t[j - 2]的匹配结果,即dp[i][j - 1]
- 如何初始化dp数组
下标0表示为空串,因此dp[0][j]
是true,dp[i][0]
除了dp[0][0]
其它都为false
- 如何遍历 => 从小到大
- 推导例子
代码
这道题也可以去记录匹配到的子串的长度,最后判断其是否和s字符串的长度相等。
1 | class Solution { |
[115] 不同的子序列
题目
给你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数,结果需要对 109 + 7 取模。
解法
- dp数组的含义
求什么就定义什么,因为求s中有多少个t,因此dp[i][j]
就表示s[i - 1]中t[j - 1]的个数
- 状态转移方程
(1)如果当前s[i - 1]和t[j - 1]相等,那么我们就要看dp[i - 1][j - 1]
的大小,比如beag和bag依赖于bea和ba中的个数,因为后者为1所以递推前者也为1。但是这是最简单的情况,因为我们可以选择不用s[i - 1]来匹配。
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
如果不选择用s[i - 1]来匹配即等于删去此字符的操作,个数就取决于s[i - 2]匹配t[j - 1]的情况,即dp[i - 1][j]
。
所以即
1 | if (s[i - 1] == t[j - 1]) |
(2)如果不等的话就单纯只有dp[i - 1][j]
- 初始化问题
dp[i][0]
表示s里以s[i - 1]为结尾的子串中空串的个数,都为1
dp[0][j]
表示空串里出现的t[j - 1]的个数,为0
其中具体问题具体分析dp[0][0]
应该是0。
- 遍历,从小到大遍历
- 例子推导
代码
1 | class Solution { |
[583] 两个字符串的删除操作
题目
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
解法
当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
情况一:删word1[i - 1],最少操作次数为dp[i - 1][j]
+ 1
情况二:删word2[j - 1],最少操作次数为dp[i][j - 1]
+ 1
情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1]
+ 2
如果不相等的情况为什么不考虑同时删除两个不相等元素即
dp[i][j] = dp[i - 1][j - 1] + 2
的情况,因为dp[i][j - 1]
的值是由dp[i - 1][j - 1] + 1
和dp[i][j - 2] + 1
以及dp[i - 1][j - 2] + 2
得来的,而且是取得最优情况,所以dp[i][j - 1] <= dp[i - 1][j - 1]
,所以没必要再在dp[i][j]
中画蛇添足加一个已经被包含了的情况进去了。
所以递推公式可简化为:dp[i][j]
= min(dp[i - 1][j]
+ 1, dp[i][j - 1]
+ 1);
代码
1 | class Solution { |
[72] 编辑距离
题目
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
解法
来自leetcode评论区的神仙解析
这题如果不是编辑距离模板题感觉根本就是hard
(1)当word1[i] == word2[j]时,由于遍历到了i和j,说明word1的0到i-1和word2的0到j-1的匹配结果已经生成,由于当前两个字符相同,因此无需做任何操作,dp[i][j] = dp[i - 1][j - 1]
(2)当word1[i] != word2[j]时,可以进行的操作有3个:
① 替换操作:可能word1的0到i-1位置与word2的0到j-1位置的字符都相同,只是当前位置的字符不匹配,进行替换操作后两者变得相同,所以此时dp[i][j] = dp[i - 1][j - 1] + 1
(这个加1代表执行替换操作)
② 删除操作:若此时word1的0到i-1位置与word2的0到j位置已经匹配了,此时多出了word1的i位置字符,应把它删除掉,才能使此时word1的0到i(这个i是执行了删除操作后新的i)和word2的0到j位置匹配,因此此时dp[i][j] = dp[i - 1][j] + 1
(这个加1代表执行删除操作)
③ 插入操作:若此时word1的0到i位置只是和word2的0到j-1位置匹配,此时只需要在原来的i位置后面插入一个和word2的j位置相同的字符使得此时的word1的0到i(这个i是执行了插入操作后新的i)和word2的0到j匹配得上,所以此时dp[i][j] = dp[i][j - 1] + 1
(这个加1代表执行插入操作)
④ 由于题目所要求的是要最少的操作数:所以当word1[i] != word2[j] 时,需要在这三个操作中选取一个最小的值赋格当前的dp[i][j]
代码
1 | class Solution { |
[712] 两个字符串的最小ASCII删除和
题目
给定两个字符串s1
和 s2
,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
解法
难得AC如此之快,编辑距离双向删减版套皮
1 | 1. dp[i][j]数组的含义:字符串s1[0: i]和字符串s2[0: j]相等需要删除的最小和 |
代码
1 | class Solution { |
回文
总结一下:
- 回文子串得定义成vector
<vector<bool>>
类型,另外求长度- 回文子序列得定义成
vector<vector<int>>
,递推求长度
[647] 回文子串
题目
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
解法
- dp数组含义
因为要统计回文的个数,所以我们需要开dp数组判断当前是否是回文,如果是的话,计数器cnt++。
回文字符串的判断老生常谈
- 状态转移方程
1 | if (s[i] == s[j]) { |
- 初始化 => 如果j从i开始遍历就无需初始化,开dp数组的时候全false即可
- 遍历顺序
i依赖于i+1,j依赖于j-1,从左下角开始遍历,j要大于i
代码
1 | class Solution { |
[516] 最长回文子序列
题目
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
解法
dp[i][j]
代表s[i: j]回文子序列长度- 状态转移方程
1 | if (s[i] == s[j]) { |
- 初始化 =>
dp[i][i] = 1
- 遍历顺序 => 左下角
代码
1 | class Solution { |