题目描述
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 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。注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。
算法分析
方法一:动态规划
思路与算法
定义 dp[i]表示以 nums[i]结尾的最长上升子序列的长度,cnt[i]表示以 nums[i] 结尾的最长上升子序列的个数。设 nums的最长上升子序列的长度为 maxLen,那么答案为所有满足 dp[i]=maxLen的 i所对应的 cnt[i]之和。
我们从小到大计算 dp数组的值,在计算 dp[i]之前,我们已经计算出 dp[0…i−1]的值,则状态转移方程为:
dp[i]=max(dp[j])+1,其中 0≤j<i 且 num[j]<num[i]
即考虑往 dp[0…i−1]中最长的上升子序列后面再加一个 nums[i]。由于 dp[j]代表 nums[0…j]中以 nums[j]结尾的最长上升子序列,所以如果能从 dp[j]这个状态转移过来,那么 nums[i]必然要大于 nums[j],才能将 nums[i]放在 nums[j]后面以形成更长的上升子序列。
对于 cnt[i],其等于所有满足 dp[j]+1=dp[i]的 cnt[j]之和。在代码实现时,我们可以在计算 dp[i]的同时统计 cnt[i]的值。
代码
class Solution { public: int findNumberOfLIS(vector<int>& nums) { int n = nums.size(); vector<int> dp(n), cnt(n); int ans = 0, maxLen = 0; for(int i =0; i < n; ++i) { dp[i] = 1; cnt[i] = 1; for(int j = 0; j < i; ++j) { if(nums[i] > nums[j]) { if(dp[j]+1 > dp[i]) { //相当于 dp[i] = max(dp[j]+1, dp[i]); dp[i] = dp[j]+1; cnt[i] = cnt[j]; }else if(dp[j]+1 == dp[i]){ //dp[j]+1 == dp[i] 有相同的长度 cnt[i] += cnt[j]; } } } if(dp[i] > maxLen) { maxLen = dp[i]; ans = cnt[i]; } else if(dp[i] == maxLen) { ans += cnt[i]; } } cout << ans << endl; return ans; } };
算法复杂度分析
时间复杂度:O(n^2),其中 n 是数组 nums的长度。
空间复杂度:O(n)。
贪心+二分查找+前缀和
下面以示例 [10,2,5,3,7,101,4,6,5] 开始。
![]()
上面的每个元素的意义:以第二列,第二个元素 3,2为例,代表了在长度为 2的递增子序列中,尾数 ≥3的上升子序列的个数 总和 为 2(2→5 和 2→3)。
对于每一行,每一列的 末尾数字 是 递增 的(2,3,4,5)。
对于每一列,元素自上向下递减。因此,我们在插入一个新元素时,
首先寻找“生长点”,(类似 leetcode300 题的思路),二分查找 每一列的尾数,找到可以延长的 最长序列。
然后,在找到的列中,二分查找 可以插入的最大尾数。则新插入的数字可以在 可以插入的最大尾数 和 比它小的任意数字 之后插入。将这些序列个数求和即可。下面的动图描述了 在之前序列 [10,2,5,3,7,101,4,6,5]的基础上,再插入 ‘7’ 的算法流程:
我们定义两个矩阵。其中:
elms[i]表示长度为 i+1的递增序列中,可能为最长递增序列做出贡献 的 所有尾数(降序排列)。
ops[i][j]表示在长度为 i+1的递增序列中,尾数≥elms[i][j]的所有上升子序列的个数总和。二分查找用 STL 库函数 lower_bound 和 upper_bound。
代码
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
vector<vector<int>> elms, ops;
for(int v : nums) {
int pos, paths = 1;
/* 一、二分查找 “生长点”。 */
if(elms.size() == 0 || v > elms.back().back()) {
pos = elms.size();
} else {
pos = lower_bound(elms.begin(), elms.end(), v, [](vector<int> &a, const int &b){
return a.back() < b;
}) - elms.begin();
}
/* 二、二分查找 “可以插入的最大尾数”。*/
if(pos > 0) {
int pre = pos - 1;
int p2 = upper_bound(elms[pre].begin(), elms[pre].end(), v, greater<int>()) - elms[pre].begin();
paths = ops[pre].back() - (p2? ops[pre][p2-1] : 0);
}
/* 三、计算以元素 v 结尾的, 长度为 pos + 1 的上升子序列个数,并累加前缀和。*/
if(pos == elms.size()) {
elms.push_back({v}), ops.push_back({paths});
} else {
elms[pos].push_back(v);
ops[pos].push_back(paths + ops[pos].back());
}
}
return ops.size()? ops.back().back() : 0;
}
};
复杂度分析
时间复杂度:O(nlog(n))。
空间复杂度:O(n)。
树状数组
我们注意到,在求 最长递增子序列的个数 的过程中,每添加一个元素,我们需要知道:
在该元素之前,比该元素小的元素中,以其为结尾的递增子序列的 最大长度 lmax;
长度 == 最大长度所对应的序列个数 cnt。则添加该元素后,以该元素为结尾的递增子序列的最大长度 = lmax+1,最大长度对应的个数 = cnt。
这和树状数组有什么关系呢?
我们定义在区间 [l,r]中的一种“值”,它包含:区间 [l,r]的最大值 max。
区间 [l,r]的最大值出现的次数 cnt。现在,考虑两个区间 [l1,r1]和 [l2,r2],两个区间没有重叠部分,两个区间的值分别为 V1和V2。如果将两个区间合并,则合并后的区间的值V12为多少?
如果 V1.max==V2.max,
则 V12.max=V1.max=V2.max,V12.cnt=V1.cnt+V2.cnt
如果 V1.max>V2.max,
则 V12.max=V1.max,V12.cnt=V1.cnt
如果 V1.max<V2.max,
则 V12.max=V2.max,V12.cnt=V2.cnt基于上述做法,我们定义一个新运算:V12=V1⊕V2。
这样,我们就可以用类似 区间和 的思路来处理 将互不重叠的 N个区间的值的合并。
代码
class Solution {
class Node {
public:
int m, c;
Node() : m(0), c(0) {}
Node& operator+=(Node& b) {
if(b.m > m)
m = b.m, c = b.c;
else if(b.m == m)
c += b.c;
return *this;
}
};
void add(Node nodes[], int rk, Node &val, int N) {
for(; rk <= N; rk += (rk & (-rk))) nodes[rk] += val;
}
Node query(Node nodes[], int rk) {
Node res;
for(; rk; rk -= (rk & (-rk))) res += nodes[rk];
return res;
}
public:
int findNumberOfLIS(vector<int>& nums) {
if(nums.size() == 0) return 0;
/* 离散化, 并用树状数组求解 */
vector<int> numsort(nums.begin(), nums.end());
sort(numsort.begin(), numsort.end());
Node *nodes = new Node[nums.size() + 1], res = Node();
for(int i : nums) {
/* 离散化, 求出当前数字的 排名 - 1 */
int rk = lower_bound(numsort.begin(), numsort.end(), i) - numsort.begin();
/* 求出当前尾数的 最长序列长度 和 个数 */
Node cur = query(nodes, rk);
cur.m++, cur.c = max(cur.c, 1);
/* 更新全局 最长序列长度 和 个数 */
res += cur;
/* 更新树状数组 */
add(nodes, rk + 1, cur, nums.size());
}
return res.c;
}
};
算法复杂度分析
时间复杂度:O(nlog(n))。
空间复杂度:O(n)。
引用链接: