LeetCode300:最长上升子序列

1. 题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是[2,3,7,101],它的长度为4

说明:

  • 可能会有多种最长上升子序列的组合,只需要输出对应的长度即可。
  • 算法时间复杂度应该为O( n 2 n^2 n2)

进阶

将算法的时间复杂度降低到O(nlogn)

2. 解题思路:动态规划

i01234567
nums[i]109253710118
dp[i]11122344
  1. 定义dp[i]:以nums[i]为结尾的最长上升子序列的长度
  2. 从dp[0]开始计算,在计算dp[i]之前,已经算出dp[0]~dp[i-1]的值。
  3. 考虑比 i 小的每一个 j ,如果有nums[j] < nums[i],那么:dp[i] = max(dp[j]) + 1;如果对所有的j,都有nums[j] >= nums[i],则:dp[i] = 1
  4. 状态转移方程为:
    dp[i] = max(dp[j]) + 1;( 0 <= j < i 且 nums[j] < nums[i] )
    dp[i] = 1; ( 0 <= j < i 且 nums[j] >= nums[i])
  5. 整个数组的最长上升子序列的长度即为所有dp[i]中的最大值:
    LIS = max(dp[i]) (0 <= i < n)

代码

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    if(n <= 1) return n;
    vector<int> dp(n,0);
    for(int i=0; i<n; i++) {
        dp[i] = 1;
        for(int j=0; j<i; j++) {
            if(nums[j] < nums[i]) {
                dp[i] = max(dp[i], dp[j]+1);
            }
        }
    }
    sort(dp.begin(), dp.end());
    return dp[n-1];
}

复杂度分析

  • 时间复杂度:O(n2),其中n为数组nums的长度。动态规划的状态数为n,计算状态dp[i]时,需要O(n)的时间遍历dp[0, … , i-1]的所有状态,所以总时间复杂度为O(n2)。
  • 空间复杂度:O(n),需要额外使用长度为n的dp数组。

3. 进阶思路:贪心+二分查找

优化思路:

考虑一个简单的贪心:如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值。

我们依次遍历数组 nums 中的每个元素,并更新数组 d。如果nums[i] > d.back() 则将nums[i]添加至数组d的末尾,否则在数组 d 中找满足 d[ans - 1] < nums[i] < d[ans] 的下标 ans,并更新 d[ans] = nums[i]。

根据数组 d 的单调性,我们可以使用二分查找寻找下标 ans,优化时间复杂度。

  • 设计思路: 假如我们需要新增一个元素nums[i],希望找出插入nums[i]之后的最长子序列。
    1. 结论一: 需要在当前允许插入(子序列尾数 < nums[i])的最长子序列后添加元素nums[i]
    2. 结论二: 只需要维护一个数组d,来保存升序子序列的最小结尾数字,这样通过比较数组d的尾数与nums[i]的大小,就可以知道是能插入数组d还是需要更新数组d的尾数。
    3. 结论三: 数组d一定是严格递增的

算法步骤:

  1. 维护一个数组d,d[i]代表长度为 i+1 的上升子序列的最小尾部元素值。
  2. 对nums[i]:如果d.back() < nums[i],在数组d末尾添加元素nums[i];如果d.back() > nums[i],需要找到数组d中第一个大于nums[i]的元素位置ans,并更新d[ans] = nums[i]。
  3. 因为数组d是有序的,因此可以通过二分法找出数组中第一个大于nums[i]的数字,及其对应下标ans。

代码

int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    if(n <= 1) return n;
    vector<int> d;
    for(int i=0; i<n; i++) {
        int k = nums[i];
        if(d.empty() || k > d.back()) {
            d.push_back(k);
        }
        else { // k <= d.back() 二分查找
            int l = 0;
            int r = d.size()-1;
            int ans = d.size();
            while (l <= r) {
                int m = l + (r - l) / 2;
                if(d[m] >= k) {
                    r = m - 1;
                    ans = m;
                }
                else {
                    l = m + 1;
                }
            }
            d[ans] = k;
        }
    }
    return d.size();
}

复杂度分析

  • 时间复杂度:O(nlogn)。数组nums的长度为n,我们依次用数组中的元素去更新数组d,更新时使用二分查找进行搜索需要O(logn),所以总时间复杂度为O(nlogn)。
  • 空间复杂度:O(n)。需要额外使用长度为n的d数组。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值