动态规划—最长递增子序列的个数(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]的值。

代码

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)。

 引用链接:

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/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值