当前位置:   article > 正文

动态规划—最长递增子序列的个数(leetcode 673)_最长递增子序列个数

最长递增子序列个数

题目描述

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 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]的值。

代码

  1. class Solution {
  2. public:
  3. int findNumberOfLIS(vector<int>& nums) {
  4. int n = nums.size();
  5. vector<int> dp(n), cnt(n);
  6. int ans = 0, maxLen = 0;
  7. for(int i =0; i < n; ++i) {
  8. dp[i] = 1;
  9. cnt[i] = 1;
  10. for(int j = 0; j < i; ++j) {
  11. if(nums[i] > nums[j]) {
  12. if(dp[j]+1 > dp[i]) { //相当于 dp[i] = max(dp[j]+1, dp[i]);
  13. dp[i] = dp[j]+1;
  14. cnt[i] = cnt[j];
  15. }else if(dp[j]+1 == dp[i]){ //dp[j]+1 == dp[i] 有相同的长度
  16. cnt[i] += cnt[j];
  17. }
  18. }
  19. }
  20. if(dp[i] > maxLen) {
  21. maxLen = dp[i];
  22. ans = cnt[i];
  23. } else if(dp[i] == maxLen) {
  24. ans += cnt[i];
  25. }
  26. }
  27. cout << ans << endl;
  28. return ans;
  29. }
  30. };

算法复杂度分析

时间复杂度: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。

 代码

  1. class Solution {
  2. public:
  3. int findNumberOfLIS(vector<int>& nums) {
  4. vector<vector<int>> elms, ops;
  5. for(int v : nums) {
  6. int pos, paths = 1;
  7. /* 一、二分查找 “生长点”。 */
  8. if(elms.size() == 0 || v > elms.back().back()) {
  9. pos = elms.size();
  10. } else {
  11. pos = lower_bound(elms.begin(), elms.end(), v, [](vector<int> &a, const int &b){
  12. return a.back() < b;
  13. }) - elms.begin();
  14. }
  15. /* 二、二分查找 “可以插入的最大尾数”。*/
  16. if(pos > 0) {
  17. int pre = pos - 1;
  18. int p2 = upper_bound(elms[pre].begin(), elms[pre].end(), v, greater<int>()) - elms[pre].begin();
  19. paths = ops[pre].back() - (p2? ops[pre][p2-1] : 0);
  20. }
  21. /* 三、计算以元素 v 结尾的, 长度为 pos + 1 的上升子序列个数,并累加前缀和。*/
  22. if(pos == elms.size()) {
  23. elms.push_back({v}), ops.push_back({paths});
  24. } else {
  25. elms[pos].push_back(v);
  26. ops[pos].push_back(paths + ops[pos].back());
  27. }
  28. }
  29. return ops.size()? ops.back().back() : 0;
  30. }
  31. };

复杂度分析

时间复杂度: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个区间的值的合并。

 代码

  1. class Solution {
  2. class Node {
  3. public:
  4. int m, c;
  5. Node() : m(0), c(0) {}
  6. Node& operator+=(Node& b) {
  7. if(b.m > m)
  8. m = b.m, c = b.c;
  9. else if(b.m == m)
  10. c += b.c;
  11. return *this;
  12. }
  13. };
  14. void add(Node nodes[], int rk, Node &val, int N) {
  15. for(; rk <= N; rk += (rk & (-rk))) nodes[rk] += val;
  16. }
  17. Node query(Node nodes[], int rk) {
  18. Node res;
  19. for(; rk; rk -= (rk & (-rk))) res += nodes[rk];
  20. return res;
  21. }
  22. public:
  23. int findNumberOfLIS(vector<int>& nums) {
  24. if(nums.size() == 0) return 0;
  25. /* 离散化, 并用树状数组求解 */
  26. vector<int> numsort(nums.begin(), nums.end());
  27. sort(numsort.begin(), numsort.end());
  28. Node *nodes = new Node[nums.size() + 1], res = Node();
  29. for(int i : nums) {
  30. /* 离散化, 求出当前数字的 排名 - 1 */
  31. int rk = lower_bound(numsort.begin(), numsort.end(), i) - numsort.begin();
  32. /* 求出当前尾数的 最长序列长度 和 个数 */
  33. Node cur = query(nodes, rk);
  34. cur.m++, cur.c = max(cur.c, 1);
  35. /* 更新全局 最长序列长度 和 个数 */
  36. res += cur;
  37. /* 更新树状数组 */
  38. add(nodes, rk + 1, cur, nums.size());
  39. }
  40. return res.c;
  41. }
  42. };

算法复杂度分析

时间复杂度:O(nlog(n))。
空间复杂度:O(n)。

 引用链接:

https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/solution/yi-bu-yi-bu-tui-dao-chu-zui-you-jie-fa-2-zui-chang/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/521388
推荐阅读
相关标签
  

闽ICP备14008679号