题目来源
题目描述
题目解析
这是一道典型的序列DP
这道题是leetcode:300. 最长递增子序列的扩展
序列DP
之前leetcode:300. 最长递增子序列相比,本题问的是最长上升子序列的个数。我们只需要在之前LIS问题的基础上通过[记录额外信息]来进行求解即可。
(1)状态定义:
在之前LIS问题中,我们定义 f [ i ] f[i] f[i]为考虑nums[i]为结尾的最长上升子序列的长度。因此答案为所有 f [ 0...... ( n − 1 ) ] f[0......(n-1)] f[0......(n−1)]中的最大值。
不失一般性地考虑 d p [ i ] dp[i] dp[i]该如何转移:
- 由于每个数都能独立成为一个子序列,因此起始必然有 d p [ i ] = i dp[i] = i dp[i]=i
- 枚举区间 [ 0 , i ) [0, i) [0,i)的所有数 n u m s [ j ] nums[j] nums[j],如果满足 n u m s [ j ] < n u m s [ i ] nums[j] < nums[i] nums[j]<nums[i],说明 n u m s [ i ] nums[i] nums[i]可以接在 n u m s [ i ] nums[i] nums[i]结尾的后面形成上升子序列,此时使用 d p [ j ] dp[j] dp[j]更新dp[i],即有 d p [ i ] = d p [ i ] + 1 dp[i] = dp[i] + 1 dp[i]=dp[i]+1
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
即位置i的最长递增子序列长度等于j从0到i-1各个位置的最长子序列+1的最大值。
回到本题,因为我们需要求解的是最长上升子序列的个数,因此需要额外定义 c n t [ i ] cnt[i] cnt[i]为考虑以 n u m s [ i ] nums[i] nums[i]结尾的最长上升子序列的个数
小结:
- dp[i]:到nums[i]为止的最长递增子序列长度
- cnt[i]:到cnt[i]为止的最长递增子序列个数
(2)状态转移
那么它的状态转移应该怎么转移呢?我们需要考虑两个维度。一个是 d p [ i ] dp[i] dp[i]的更新,一个是 c n t [ i ] cnt[i] cnt[i]的更新。
那么如何更新 c n t [ i ] cnt[i] cnt[i]呢?
- 以 n u m s [ i ] nums[i] nums[i]结尾的数字,最长递增子序列的个数是cnt[i]
- 那么在 n u m s [ i > n u m s [ j ] ] nums[i > nums[j]] nums[i>nums[j]]的前提下,如果在 [ 0 , i − 1 ] [0, i-1] [0,i−1]的范围内,找到了 j j j,使得 d p [ j ] + 1 > d p [ i ] dp[j] + 1 > dp[i] dp[j]+1>dp[i],说明找到了一个更长的以 n u m s [ i ] nums[i] nums[i]结尾的递增子序列,此时 n u m s [ i ] nums[i] nums[i]要追加到之前 [ . . . . . n u m s [ j ] ] [.....nums[j]] [.....nums[j]]的后面,形成 [ . . . . . . n u m s [ j ] , n u m s [ i ] [......nums[j], nums[i] [......nums[j],nums[i]
- 那么以 n u m s [ j ] nums[j] nums[j]为结尾的最长递增子序列的个数,就是最新的以nums[i]为结尾的子串的最长递归子序列的个数,即 c n t [ j ] = c n t [ i ] cnt[j] = cnt[i] cnt[j]=cnt[i]
- 在 n u m s [ i ] > n u m s [ j ] ] nums[i] > nums[j]] nums[i]>nums[j]]的前提下,如果在 [ 0 , i − 1 ] [0, i-1] [0,i−1]的范围内,找到了 j j j,使得 d p [ j ] + 1 = d p [ i ] dp[j] + 1 = dp[i] dp[j]+1=dp[i],说明找到了两个相同长度的递增子序列,那么以nums[i] 为结尾的子串的最长递增子序列的个数 就应该加上以nums[j]]为结尾的子串的最长递增子序列的个数,即: c n t [ i ] + = c n t [ j ] ; cnt[i] += cnt[j]; cnt[i]+=cnt[j];
小结, c n t [ i ] cnt[i] cnt[i]该如何转移:
- 由于每个数都能独自一个称为子序列,因此起始必然有 c n t [ i ] = 1 cnt[i] = 1 cnt[i]=1
- 枚举区间
[
0
,
i
)
[0, i)
[0,i)的所有数
n
u
m
s
[
j
]
nums[j]
nums[j],如果满足
n
u
m
s
[
j
]
<
n
u
m
s
[
i
]
nums[j] < nums[i]
nums[j]<nums[i],说明
n
u
m
s
[
i
]
nums[i]
nums[i]可以接在
n
u
m
s
[
i
]
nums[i]
nums[i]结尾的后面形成上升子序列,这时候对
d
p
[
i
]
dp[i]
dp[i]和
d
p
[
i
+
1
]
dp[i + 1]
dp[i+1]的大小关系分情况讨论:
- 满足 d p [ i ] < d p [ i ] + 1 dp[i] < dp[i] + 1 dp[i]<dp[i]+1:说明最长递增子序列的长度增加了,dp[i] = dp[j] + 1,长度增加,数量不变 cnt[i] = cnt[j]
- 满足 d p [ i ] = d p [ i ] + 1 dp[i] = dp[i] + 1 dp[i]=dp[i]+1:说明最长递增子序列的长度并没有增加,但是出现了长度一样的情况,数量增加 cnt[i] += cnt[j]
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
cnt[i] = cnt[j];
} else if (dp[j] + 1 == dp[i]) {
cnt[i] += cnt[j];
}
dp[i] = max(dp[i], dp[j] + 1);
}
当然也可以这么写:
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1; // 更新dp[i]放在这里,就不用max了
cnt[i] = cnt[j];
} else if (dp[j] + 1 == dp[i]) {
cnt[i] += cnt[j];
}
}
(3)初始化:
- dp[i]记录了i之前(包括i)最长递增序列的长度。最小的长度也是1,所以dp[i]初始为1。
- cnt[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。那么最少也就是1个,所以cnt[i]初始为1。
vector<int> dp(nums.size(), 1);
vector<int> count(nums.size(), 1);
(4)遍历顺序:
- dp[i] 是由0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
- j其实就是0到i-1,遍历i的循环里外层,遍历j则在内层
(5)返回结果
- 题目要求最长递增序列的长度的个数,我们应该把最长长度记录下来
- 然后遍历dp数组,如果dp数组记录的最大长度dp[i]等于max_length,将对应的数量count[i]加到结果res中
代码如下:
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
vector<int> count(nums.size(), 1);
int maxCount = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
}
if (dp[i] > maxCount) maxCount = dp[i];
}
}
int result = 0;
for (int i = 0; i < nums.size(); i++) {
if (maxCount == dp[i]) result += count[i];
}
return result;
}
};
本题通过存储pair类型的数组,pair的first存储当前位置的最长长度,pair的second存储达到这个最长长度的个数。
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int size = nums.size();
vector<pair<int, int>> dp(size, {0,0});
int max_value = 1;
int cnt = 0;
for(int i = 0; i < size; i++){
dp[i] = {1,1};
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
//相等说明达到这个长度还有不同路径实现
if(dp[i].first == dp[j].first + 1){
dp[i].second += dp[j].second;
}
//不相等则说明需要重新更新最大长度,此时的条数等于前一个状态的条数,因为达到前一个状态可以有前一个状态的条数,达到此状态条数和原来的条数在更新的时候是相等的。
if(dp[i].first < dp[j].first + 1){
dp[i].second = dp[j].second;
}
dp[i].first = max(dp[i].first, dp[j].first + 1);
max_value = max(dp[i].first, max_value);
}
}
}
for(int i = 0; i < size; i ++){
if(dp[i].first == max_value){
cnt += dp[i].second;
}
}
return cnt;
}
};
思路二
小结
求最长连续递增序列
给定一个为排序的整数数组,找到最长而且连续的递增序列
- dp[i]表示表示前i个序列(以nums[i]结尾的)最长连续递增序列的长度
- 只需要关注前一个位置
i
i
i与前一个位置
i
−
1
i - 1
i−1的值的大小
- n u m s [ i ] > n u m s [ i + 1 ] nums[i] > nums[i + 1] nums[i]>nums[i+1]时, i i i至少可以与 i − 1 i - 1 i−1形成一个连续递增序列,因为它们两是挨着的并且递增的,因此 d p [ i ] = d p [ i − 1 ] + 1 dp[i] = dp[i - 1] + 1 dp[i]=dp[i−1]+1
- n u m s [ i ] < = n u m s [ i + 1 ] nums[i] <= nums[i + 1] nums[i]<=nums[i+1]时,不能形成连续递增序列,所以dp[i]与dp[i-1]无关
可以看到,dp[i]的状态只依赖前一个状态
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()){
return 0;
}
//用来存储预期的结果
std::vector<int> dp(nums.size(), 1);//至少可以与其自身形成递增序列
int max = 1;
for (int i = 1; i < nums.size(); ++i) {
if(nums[i] > nums[i - 1]){
dp[i] = dp[i - 1] + 1;
max = std::max(max, dp[i]);
}
}
return max;
}
};
求最长连续递增子序列
- dp[i]表示前i个序列(以nums[i]结尾的)最长连续递增子序列的长度
- dp[i]的状态可能从dp[0…i-1]之前的状态转移而来,也就是需要关注num[0…i-1]与nums[i]的关系