目录
题目一——300. 最长递增子序列 - 力扣(LeetCode)
题目三—— 673. 最长递增子序列的个数 - 力扣(LeetCode)
题目四——646. 最长数对链 - 力扣(LeetCode)
题目五——1218. 最长定差子序列 - 力扣(LeetCode)
题目六——873. 最长的斐波那契子序列的长度 - 力扣(LeetCode)
题目七——1027. 最长等差数列 - 力扣(LeetCode)
题目八——446. 等差数列划分 II - 子序列 - 力扣(LeetCode)
题目九——P2285 [HNOI2004] 打鼹鼠 - 洛谷
题目十——P2196 [NOIP 1996 提高组] 挖地雷 - 洛谷
题目一——300. 最长递增子序列 - 力扣(LeetCode)
首先子序列不是子数组!!
子序列 是可以通过从另一个数组删除或不删除某些元素,但不更改其余元素的顺序得到的数组。
说白了就是按从左往右的顺序挑选元素,组成的新数组就是子序列。每个元素的相对顺序是保持不变的!!!
我们下面看一个例子:
原数组:[1, 2, 3, 4, 5]
可能的子序列:
[1, 2, 3]
(保留了前三个元素)[1, 3, 5]
(跳过了第二个和第四个元素)[1, 2, 4, 5]
(跳过了第三个元素)[5]
(只保留了最后一个元素)[1, 2, 3, 4, 5]
(没有删除任何元素)[]
(空数组也是任何数组的子序列,表示删除所有元素)
有没有发现,子序列就是包含了子数组。所以子序列的问题是比子数组的题目难的!!
1.状态表示:
对于线性 dp ,我们可以⽤「经验+题⽬要求」来定义状态表⽰:
- i. 以某个位置为结尾,巴拉巴拉;
- ii. 以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
使用动态规划(DP)数组 dp
,其中 dp[i]
表示以 nums[i]
结尾的所有递增子序列中的最长长度。
2.状态转移方程:
像这种子数组或者子序列的状态转移方程的推导,都是从如何构成子序列或者子数组来解决的!!
我们看dp[i]
表示以 nums[i]
结尾的所有递增子序列中的最长长度
那以nums[i]为结尾的子序列可以分为下面几类:
- 单独一个元素组成子序列:
- 和前面的元素一起组成子序列(比如说跟在nums[i-1]后面形成子序列,或者跟在nums[i-2]后面,再或者跟在nums[i-3]后面……跟在nums[0]都是可以的
所以我们很容易就能推导出下面这个状态转移方程
- 单独一个元素组成子序列:dp[i]=1;
- 和前面的元素一起组成子序列:这个时候我们不能随便乱填dp[i],我们可以回去看一下状态表示:一定要是最长子序列的长度,所以我们完全可以定义一个变量 j 从0开始往n-1进行遍历,看看哪个的dp[j]最大,哪个的最大就让nums[i]跟在它后面。所以就是最大的dp[j] + 1
由于我们要找最长的,所以最终的状态转移方程:
if (nums[i] > nums[j]) {
dp[i] = max(dp[i],dp[j] + 1);
}
注意:我们回到这个题目,题目说找一个严格递增的子序列,所谓严格递增,就是子序列里面各个元素不能存在相等的情况,只能是nums[i]>nums[j]。
3.初始化:
初始化我们是将它初始化为最差的情况。
每个元素单独都能构成一个递增子序列,因此初始时,dp[i]
应该被设置为 1(对于所有 i
)。
4.填表顺序:
由于我们需要用到前面的状态来计算当前状态,因此填表顺序应该是从左往右(即从小到大的索引)。
5.返回值:
最长递增子序列的长度就是 dp
数组中的最大值。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);
int ret=1;
for (int i = 1; i < n; i++) {
for (int j = 0; j <= i-1; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i],dp[j] + 1);
}
ret=max(ret,dp[i]);
}
}
return ret;
}
};
题目二——376. 摆动序列 - 力扣(LeetCode)
这个题目很像多状态dp啊
1. 状态表⽰:
对于线性 dp ,我们可以⽤「经验+题⽬要求」来定义状态表⽰:
- i. 以某个位置为结尾,巴拉巴拉;
- ii. 以某个位置为起点,巴拉巴拉。
这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:
- dp[i] 表⽰「以 i 位置为结尾的最⻓摆动序列的⻓度」。
但是,问题来了,如果状态表⽰这样定义的话,以 i 位置为结尾的最⻓摆动序列的⻓度我们没法 从之前的状态推导出来。因为我们不知道前⼀个最⻓摆动序列的结尾处是递增的,还是递减的。因 此,我们需要状态表⽰能表⽰多⼀点的信息:要能让我们知道这⼀个最⻓摆动序列的结尾是递增的 还是递减的。
解决的⽅式很简单:搞两个 dp 表就好了。
- f[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置呈现「上升趋势」的最⻓摆 动序列的⻓度;
- g[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置呈现「下降趋势」的最⻓摆 动序列的⻓度。
2.状态转移方程
像这种子数组或者子序列的状态转移方程的推导,都是从如何构成子序列或者子数组来解决的!!
那以nums[i]为结尾的子序列可以分为下面几类:
- 单独一个元素组成子序列:
- 和前面的元素一起组成子序列(比如说跟在nums[i-1]后面形成子序列,或者跟在nums[i-2]后面,再或者跟在nums[i-3]后面……跟在nums[0]都是可以的
由于子序列的构成比较特殊,对于 f[i]
(即以 i
位置为结尾的子序列),其前一个位置 j
可以是 [0, i - 1]
区间内的某一个位置。我们可以根据子序列的构成方式进行分类讨论:
-
单独一个元素组成子序列:此时,只能由单个元素自身构成,因此
[0, i - 1]
的任意f[i]
初始化为 1。 -
和前面的元素一起组成子序列:因为结尾要呈现上升趋势,所以需要
nums[j] < nums[i]
。在满足这个条件下,j
结尾的子序列需要呈现下降状态,因此最长的摆动序列就是g[j] + 1
。我们需要找出所有满足条件下的最大g[j] + 1
,并更新f[i]
。
综上,对于 f[i]
的更新公式为:
f[i] = max(g[j] + 1, f[i])
,其中 j
满足 0 <= j < i
且 nums[j] < nums[i]
。
同理,对于 g[i]
(即以 i
位置为结尾的子序列),我们也可以进行分类讨论:
-
单独一个元素组成子序列:此时,只能由单个元素自身构成,因此
g[i]
初始化为 1。 -
和前面的元素一起组成子序列:因为结尾要呈现下降趋势,所以需要
nums[j] > nums[i]
。在满足这个条件下,j
结尾的子序列需要呈现上升状态,因此最长的摆动序列就是f[j] + 1
。我们需要找出所有满足条件下的最大f[j] + 1
,并更新g[i]
。
综上,对于 g[i]
的更新公式为:
g[i] = max(f[j] + 1, g[i])
,其中 j
满足 0 <= j < i
且 nums[j] > nums[i]
。
3.初始化:
由于每个元素单独都能构成一个摆动序列(长度为 1),因此我们可以将 f
和 g
两个 DP 表内的所有元素初始化为 1。
4.填表顺序:
毫无疑问,我们应该从左往右依次填充 DP 表,即按照数组 nums
的顺序进行。
5.返回值:
最终,我们应该返回两个 DP 表里面的最大值,这代表了最长的上升摆动子序列和下降摆动子序列中的最长者。我们可以在填表的过程中,顺便更新一个全局的最大值变量来记录这个结果。
所以我们很容易就想到下面这个代码
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
vector<int>f(n,1);
vector<int>g(n,1);
int ret=1;
for(int i=1;i<n;i++)
{
for(int j=0;j<=i-1;j++)
{
if(nums[j]<nums[i])
{
f[i]=max(f[i],g[j]+1);
}