首先我们要明确以下两个问题:
子序列:子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
子数组:子数组是数组中的一个连续部分
先给此类题目定一个基调,首先编辑距离相关的问题也属于序列和子序列问题,并且他们除了递推公式之外,还需要想明白一个很重要的问题,那就是递推公式成立的条件。
好了现在开始做总结!
首先我们一起复习动规五部曲:
- 确定 dp 数组以及下标含义
- 确定递推公式
- dp 数组如何初始化
- 确定遍历顺序
- 距离推导 dp 数组
⭐️300.最长递增子序列
文章讲解:300.最长递增子序列
视频链接:动态规划之子序列问题,元素不连续!| LeetCode:300.最长递增子序列
状态:子序列问题的开篇,主要是子序列问题思路的开端,非常重要。特别是关于他的dp数组,反复推敲反复琢磨:dp[i]
表示i
之前包括i
的以nums[i]
结尾的最长递增子序列的长度
这里需要提到的一个重点是:
对于递推公式:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
j
]
+
1
)
dp[i] = max(dp[i], dp[j] + 1)
dp[i]=max(dp[i],dp[j]+1)
这里的主要含义不是取其中的最大值,而是去遍历 dp[j]
,拿到他的最大值。
既然这里有个 j 出现了,我们当然可以判断到:
if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1)
所以遍历顺序也很容易得到:
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < nums.size(); j++) {
...
}
}
674.最长连续递增序列
文章讲解:674.最长连续递增序列
视频讲解:动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列
状态:连续递增子序列和递增子序列区别在哪里?体现在代码中的话又在哪里呢?
- 首先之前的300.最长递增子序列只要求递增子序列,并不要求序列在数组是是一组连续的数,而本题要求连续的递增子序列。其实只要一连续,很明显就可以用贪心算法了(贪心算法的求解会放在最后面);
- 再一个,代码中我们要求的连续的子序列,所以两次遍历肯定是不用了,只要每次发现连续小的话,直接来个+1即可。
这里我们可以使用贪心算法,某种程度来说,也可以使用滑动窗口来解决该问题,也就是说,只要一连续,尝试滑动窗口也是非常不错的选择。
但是在这里只描述动态规划的方法:
-
明确dp数组的含义
跟上一题一样,以下标i为结尾的最长连续递增子序列的长度为dp[i] -
确定递推公式
在300.最长递增子序列中,我们的dp[i]是由i面前的某个元素j来进行推导。
本题中我们求的是连续的递增子序列,所以我们没有必要去比较前面的所有元素了,在上一题中,我们可是遍历了0~i-1的j。
所以如果我们nums[i] > nums[i-1],我们就做对应的那个dp[i] + 1的操作,即:
d p [ i ] = d p [ i − 1 ] + 1 dp[i]=dp[i-1]+1 dp[i]=dp[i−1]+1 -
dp数组的初始化
以下标i为结尾的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。
所以dp[i]应该初始1; -
确定遍历顺序
从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。
⭐️718.最长重复子数组
文章讲解:718.最长重复子数组
视频讲解:动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组
状态:题目风格开始突变!现在我们要比较的是两个数组!
其实如果我们能够提前知道这题使用动态规划,我们就可以拿 dp 数组去硬套!
题目要求返回 两个数组中 公共的 、长度最长的子数组的长度 。
我们可以设 dp 的含义为:dp[i][j] 表示分别以第 i - 1 和第 j - 1 结尾的数组 A 和数组 B公共的、长度最长的子数组。为什么这么设置呢?因为我们需要统一递推公式。等我们推理出递推公式之后自然可以理解。
那么递推公式应该是什么样的呢?其实我们对着两个数组稍微比较一下就知道了,如果想推理出第 i -1 个位置和第 i - 2 个位置的值,我们需要在前一个位置上 + 1,所以有:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
d
p
[
j
−
1
]
+
1
dp[i][j] = dp[i - 1]dp[j - 1] + 1
dp[i][j]=dp[i−1]dp[j−1]+1
如果我们在设计 dp 数组含义的时候使用 dp[i][j] 表示分别以第 i 和第 j 结尾,那么我们如何计算 dp[0][0]呢?
至此,题目基本解决。
1143.最长公共子序列
文章讲解:1143.最长公共子序列
视频讲解:动态规划子序列问题经典题目 | LeetCode:1143.最长公共子序列
状态:本题其实就跟718.最长重复子数组类似,但是不要求连续了。
本题的难点我认为主要还是在:
- 递推数组的分情况讨论:
我们如何得出需要讨论这两个情况呢?因为我们对本题的 dp 数组的设计为:长度为[0, i - 1]
的字符串text1与长度为[0, j - 1]
的字符串text2的最长公共子序列为dp[i][j]
。
如果 i - 1 位置 和 j - 1 位置相等了,我们肯定就是把 i - 2 位置和 j - 2 位置的值 + 1;
如果不想等,那么说明我们不能考虑同时考虑这两个位置的字母,选择比较 i - 2 与 j - 1 位置 or i - 1 位置 与 j - 2 位置-
i f ( t e s t [ i − 1 ] = = t e x t 2 [ j − 1 ] ) if (test[i - 1] == text2[j - 1]) if(test[i−1]==text2[j−1])
-
i f ( t e s t [ i − 1 ] ! = t e x t 2 [ j − 1 ] if (test[i - 1] != text2[j - 1] if(test[i−1]!=text2[j−1]
-
- 理解第二个递推公式
⭐️647.回文子串
说实话,回文子串这道题更适合使用双指针来完成,其实就是确定一个基准点,然后左右指针忘两边扩,如果是回文子串则进行记录,不是就跳出子循环。
文章链接:647.回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
在这里我们只讲动态规划的解法。
这里最强的就是使用布尔类型的 dp 数组,dp[i][j]
:表示区间范围[i,j]
(注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]
为true,否则为false。
下面我们一起考虑一下递推公式:
回文子串一个最基础的判定肯定就是从中间到两边散开,s[i] == s[j]
,所以我们的递推的条件就是,如果二者相等,我们开始计算 dp[i][j]:
if s[i] == s[j]:
1. i = j,下标相同,当然是回文子串
2. i 、j,相差1,比如说aa,也是回文子串
3. i、j,相差大于1,那么dp[i][j]为回文子串的条件是,dp[i+1][j-1]是回文子串,所以递推代码如下
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1]) {
ans++;
dp[i][j] = true;
}
并且我们需要稍微关注一下遍历顺序,我们推理方向为 i + 1 ==> i、j - 1 ==>j
所以很明显是从下到上,从左到右:
int len = s.length();
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) {
...
}
}
并且在这里 i 和 j 是可以相等的,这一点要特别注意,因为下一题二者就不能相等了。
为什么他们两个能相等呢?因为我们在 dp[i + 1][j - 1] 之前就做了 j - i <= 1 的判断,所以是允许的。
516最长回文子序列
好了,经典的序列问题又来了
文章链接:516最长回文子序列
延续上一题的思路,只不过此时我们不能使用布尔类型的 dp 数组。因为这个最大值的计算肯定不能直视简单的 ans++ 就能完成的,所有的子序列问题都不能。
那么基本的设置思路和上一题一样:dp[i][j]
----字符串s
在[i, j]
范围内最长的回文子序列的长度为dp[i][j]
这里我们一起来推理递推公式:
首先,如果我们遍历的过程中有 s[i] == s[j]
,那么我们本能得知道,只要我们删除 [i, j] 范围中不是回文的字母,就应该有 dp[i][j] = xxx + 2
;
然后,如果 s[i] != s[j]
,我们dp[i][j]的含义是[i, j] 范围内的最长子序列长度,那么就有:dp[i][j] = max(dp[i +1][j], dp[i][j - 1])
,这里我们不做过多解释,之前也用过类似思路,既然同时考虑 i, j 不满足条件,那么我们就只考虑其中一项,然后取最值。基于这一步,我们做回推,当 s[i] == s[j]
满足时,就有 dp[i][j] = dp[i + 1]dp[j - 1] + 2; 因为 dp[i + 1][j - 1] 肯定已经是 dp 数组的最大回文子序列了。
综上所述:
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
本题我认为最重要的还是要关注 dp 数组的初始化
从递推公式可以看出,递推公式计算不到i 和j相同时候的情况。
当i与j相同,那么dp[i][j]
一定是等于1的,即:一个字符的回文子序列长度就是1。
其他都应该初始化成0,这样题对公式dp[i][j]=max(dp[i + 1][j], dp[i][j - 1]);
才不会被初始值覆盖
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
最后我们不得不提,此时在做遍历顺序时,j 千万不能等于 i ,因为我们是没有做任何前置判断的。
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); j++) {
...
}
}