300. 最长递增子序列 ●●

本文介绍了如何使用动态规划和二分查找算法解决最长递增子序列(LIS)问题。动态规划方法通过维护一个dp数组记录以每个数字为结尾的最长递增子序列长度,而二分查找方法通过优化状态定义,利用排序列表降低搜索复杂度。两种方法的时间复杂度分别为O(n^2)和O(nlogn),空间复杂度均为O(n)。
摘要由CSDN通过智能技术生成

300. 最长递增子序列 ●●

描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

题解

1. 动态规划
  1. dp[i] 表示以第 i 个数字nums[i]为结尾的最长递增序列长度;
  2. if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
    位置 i 的最长升序子序列等于j从0到 i-1各个位置的最长升序子序列 + 1 的最大值。
    注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值
  3. 每一个 i,对应的dp[i](即最长上升子序列)起始大小至少都是1;
  4. 双层循环,从前往后遍历。
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n 为数组 nums 的长度。动态规划的状态数为 n,计算状态 dp[i] 时,需要 O(n) 的时间遍历 dp[0…i−1] 的所有状态。
  • 空间复杂度:O(n),需要额外使用长度为 n 的 dp 数组。

在这里插入图片描述

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        int ans = 1;
        vector<int> dp(n, 1);   	// dp[i] 表示以第i个数字为结尾的最长递增序列长度
        for(int i = 1; i < n; ++i){	// nums[i]结尾
            for(int j = 0; j < i; ++j){
                if(nums[i] > nums[j]){
                    dp[i] = max(dp[i], dp[j] + 1);	// 升序时,0到i-1各个位置的最长升序子序列 + 1
                }
            }  
            ans = max(ans, dp[i]);	// 取最大值
        }
        return ans;
    }
};

2. DP + 二分查找

通过重新设计状态定义,使整个 dp 为一个排序列表;这样在计算每个 dp[k] 时,就可以通过二分法遍历 [0,k) 区间元素,将此部分复杂度由 O(N) 降至 O(logN)。

定义 dp[i] 表示最长递增序列长度为 i+1 的最小末尾数,如 {1, 2, 4, 3} 中,最长递增序列长度为 3 的最小末尾数 dp[2] = 3;

此时 dp 数组一定是严格递增的,即 k < n 时有dp[ k ] < dp[ n ],当尽可能使每个子序列尾部元素值最小的前提下,子序列越长,其序列尾部元素值一定更大。(可用反证法证明)。

步骤为:

  1. 新建dp数组,用于存放最长递增序列长度为 i+1 的最小末尾数,初始化dp[0] = nums[0];
  2. 遍历数组nums,对每个元素进行比较插入操作,
    1)如果dp中元素都小于它,则尾部插入新元素;
    2)否则,用二分法找到第一个大于等于 nums[i] 的位置,进行替换。

总之,思想就是让 dp[i] 存储序列长度为 i+1 的最小末尾数。这样,dp 未必是真实的最长上升子序列,但其长度就是最终的子序列长度。 比如 {1, 2, 9, 10, 3, 4, 5} 的替换过程。

  • 时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN) : 遍历 nums 列表需 O(N),在每个 nums[i] 二分法需 O(logN)。
  • 空间复杂度 O(N) : tails 列表占用线性大小额外空间。

在这里插入图片描述

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp;                 // dp[i] 表示最长递增序列长度为i+1的最小末尾数
        dp.emplace_back(nums[0]);		// dp初始化
        for(int i = 1; i < n; ++i){
            int l = 0;
            int r = dp.size()-1;
            if(dp[r] < nums[i]){        // dp数组最大数小于nums[i],尾部插入元素
                dp.emplace_back(nums[i]);
                continue;
            }                           // nums[i]在dp数组范围内
            while(l <= r){              // 二分查找第一个大于等于nums[i]的位置,进行替换
                int m = l + (r-l) / 2;
                if(dp[m] >= nums[i]){   // 若非要求严格递增,将此行>=改为>,行10的<改为<=
                    r = m - 1;
                }else{
                    l = m + 1;
                }
            }
            dp[l] = nums[i];            // 元素替换
        }
        return dp.size();
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值