算法沉淀——动态规划之子序列问题
01.最长递增子序列
题目链接:https://leetcode.cn/problems/longest-increasing-subsequence/
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
思路
1. 状态表达: 我们定义一个动态规划数组 dp
,其中 dp[i]
表示以第 i
个位置元素为结尾的所有「子序列」中,最长递增子序列的长度。
2. 状态转移方程: 针对 dp[i]
的状态,我们考虑「子序列的构成方式」进行分类讨论:
- 当子序列长度为
1
时,只包含自身,因此dp[i] = 1
; - 当子序列长度大于
1
时,我们遍历前面的所有元素j
(0 <= j <= i - 1
),检查是否可以将第i
个位置的元素接在第j
个元素之后形成递增子序列。如果nums[j] < nums[i]
,说明可以构成递增序列,更新状态:dp[i] = max(dp[j] + 1, dp[i])
。
3. 初始化: 初始时,每个元素都可以构成长度为 1
的递增子序列,因此将整个 dp
数组初始化为 1
。
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;j++){
if(nums[j]<nums[i])
dp[i]=max(dp[i],dp[j]+1);
}
ret=max(ret,dp[i]);
}
return ret;
}
};
02.摆动序列
题目链接:https://leetcode.cn/problems/wiggle-subsequence/
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
思路
1. 状态表达: 我们定义两个动态规划数组 f
和 g
,其中:
f[i]
表示以第i
个位置元素为结尾的所有的子序列中,最后一个位置呈现「上升趋势」的最长摆动序列的长度。g[i]
表示以第i
个位置元素为结尾的所有的子序列中,最后一个位置呈现「下降趋势」的最长摆动序列的长度。
2. 状态转移方程: 对于 f[i]
的状态,我们考虑子序列的构成方式:
- 当子序列长度为
1
时,只包含自身,因此f[i] = 1
。 - 当子序列长度大于
1
时,遍历前面的所有元素j
(0 <= j <= i - 1
),检查是否可以将第i
个位置的元素接在第j
个元素之后形成递增子序列。如果nums[j] < nums[i]
,则在满足这个条件下,要使j
结尾呈现下降状态,最长的摆动序列即为g[j] + 1
。更新状态:f[i] = max(g[j] + 1, f[i])
。
对于 g[i]
的状态,我们同样考虑子序列的构成方式:
- 当子序列长度为
1
时,只包含自身,因此g[i] = 1
。 - 当子序列长度大于
1
时,遍历前面的所有元素j
(0 <= j <= i - 1
),检查是否可以将第i
个位置的元素接在第j
个元素之后形成递减子序列。如果nums[j] > nums[i]
,则在满足这个条件下,要使j
结尾呈现上升状态,最长的摆动序列即为f[j] + 1
。更新状态:g[i] = max(f[j] + 1, g[i])
。
3. 初始化: 初始时,每个元素都可以构成长度为 1
的摆动序列,因此将整个 f
和 g
数组初始化为 1
。
4. 填表顺序: 按照从左到右的顺序遍历数组。
5. 返回值: 返回 f
和 g
数组中的最大值。这样的动态规划算法通过遍历数组,不断更新状态,最终得到以每个位置为结尾的子序列中最长的摆动序列长度。最后返回两个数组中的最大值。
代码
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
vector<int> f(n,1),g(n,1);
int ret=1;
for(int i=1;i<n;++i){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]) f[i]=max(g[j]+1,f[i]);
else if(nums[j]>nums[i]) g[i]=max(f[j]+1,g[i]);
}
ret=max(ret,max(g[i],f[i]));
}
return ret;
}
};
03.最长递增子序列的个数
题目链接:https://leetcode.cn/problems/number-of-longest-increasing-subsequence/
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
提示:
1 <= nums.length <= 2000
-106 <= nums[i] <= 106
思路
1. 状态表达: 我们定义两个动态规划数组 len
和 count
,其中:
len[i]
表示以第i
个位置为结尾的最长递增子序列的长度。count[i]
表示以第i
个位置为结尾的最长递增子序列的个数。
2. 状态转移方程: 首先,对于 len[i]
的状态,我们考虑子序列的构成方式:
- 当子序列长度为
1
时,只包含自身,因此len[i] = 1
。 - 当子序列长度大于
1
时,遍历前面的所有元素j
(0 <= j < i
),检查是否可以将第i
个位置的元素接在第j
个元素之后形成递增子序列。如果nums[j] < nums[i]
,则在满足这个条件下,要使j
结尾呈现上升状态,最长的递增子序列即为len[j] + 1
。更新状态:len[i] = max(len[j] + 1, len[i])
。
接着,对于 count[i]
的状态,我们考虑子序列的构成方式:
- 我们此时已经知道
len[i]
的信息,还知道[0, i - 1]
区间上的count[j]
信息,使用j
表示[0, i - 1]
区间上的下标。 - 我们可以再遍历一遍
[0, i - 1]
区间上的所有元素,只要能够构成上升序列,并且上升序列的长度等于len[i]
,那么我们就把count[i]
加上count[j]
的值。这样循环一遍之后,count[i]
存的就是我们想要的值。更新状态:count[i] += count[j]
,其中0 <= j < i
且nums[j] < nums[i] && len[j] + 1 == len[i]
。
3. 初始化:
- 对于
len[i]
,所有元素自己就能构成一个递增序列,直接全部初始化为1
。 - 对于
count[i]
,我们先初始化为0
,然后在累加的时候判断。
4. 填表顺序: 从左往右遍历数组。
5. 返回值: 最终的最长递增子序列的长度为 maxLen
。根据题目要求,返回所有长度等于 maxLen
的子序列的个数。
代码
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> len(n, 1), count(n, 1);
int retlen = 1, retcount = 1;
for (int i = 1; i < n; i++)
{
for (int j = 0; j < i; j++)
{
if (nums[j] < nums[i])
{
if (len[j] + 1 == len[i])
count[i] += count[j];
else if (len[j] + 1 > len[i])
len[i] = len[j] + 1, count[i] = count[j];
}
}
if (retlen == len[i])
retcount += count[i];
else if (retlen < len[i])
retlen = len[i], retcount = count[i];
}
return retcount;
}
};
04.最长数对链
题目链接:https://leetcode.cn/problems/maximum-length-of-pair-chain/
给你一个由 n
个数对组成的数对数组 pairs
,其中 pairs[i] = [lefti, righti]
且 lefti < righti
。
现在,我们定义一种 跟随 关系,当且仅当 b < c
时,数对 p2 = [c, d]
才可以跟在 p1 = [a, b]
后面。我们用这种形式来构造 数对链 。
找出并返回能够形成的 最长数对链的长度 。
你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例 1:
输入:pairs = [[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4] 。
示例 2:
输入:pairs = [[1,2],[7,8],[4,5]]
输出:3
解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。
提示:
n == pairs.length
1 <= n <= 1000
-1000 <= lefti < righti <= 1000
思路
- 排序: 首先,对数对数组按照每个数对的第一个元素进行升序排序。这是为了方便后续动态规划的处理。
- 状态表达: 我们定义动态规划数组
dp
,其中dp[i]
表示以第i
个数对为结尾的最长数对链的长度。 - 状态转移方程: 对于
dp[i]
,遍历所有[0, i - 1]
区间内的数对,用j
表示下标。找出所有满足pairs[j][1] < pairs[i][0]
的j
,然后找出其中最大的dp[j]
,最后加上1
就是以第i
个数对为结尾的最长数对链。状态转移方程为:dp[i] = max(dp[j] + 1, dp[i])
,其中0 <= j < i
。 - 初始化: 刚开始的时候,全部初始化为
1
。 - 填表顺序: 根据状态转移方程,填表顺序是从左往右。
- 返回值: 根据状态表达,返回整个
dp
数组中的最大值。
代码
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(),pairs.end());
int n=pairs.size();
vector<int> dp(n,1);
int ret=1;
for(int i=1;i<n;i++){
for(int j=0;j<i;j++){
if(pairs[i][0]>pairs[j][1]) dp[i]=max(dp[i],dp[j]+1);
}
ret=max(ret,dp[i]);
}
return ret;
}
};