最长上升子序列

前言:本人某天在b站刷到有up主讲解这种类型的题,觉得这是一种常用的模型,特此写下本文

题目:力扣 300.最长递增序列

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

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

示例 1:

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

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -10^{4} <= nums[i] <= 10^{4}

emmm我刚开始想暴力来着,看了眼数据范围感觉不妙,毕竟好歹是力扣中等题qwq

方法一:DP

我们假设dp[i]是以nums[i]结尾的长度最长的最长上升子序列,那么好,我们可以从前往后遍历dp[i]

在遍历到dp[i]的时候,我们要明白,dp[0......i-1]我们是知道的,我们从0....i-1用j遍历,当nums[j]<nums[i]的情况下我们找到最大的dp[j],在dp[j]的基础上+1就是dp[i]

状态转移方程:dp[i]=max(dp[i],dp[j]+1)

前提:0<=j<i  && num[j]<num[i]

上代码:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
       vector<long long> dp(n,0);
       if(n==0){
        return 0;
       }
       for(int i=0;i<n;i++)
       {
          dp[i]=1;//自身可以是一个长度为1的上升子序列
          for(int j=0;j<i;j++)
          {
            if(nums[j]<nums[i])
            {
                dp[i]=max(dp[i],dp[j]+1);
            }
          }
       }
       return *max_element(dp.begin(),dp.end());
    }
};
//时间复杂度:o(n^2)

但很遗憾 时间复杂度是0(n^2),我们需要找到时间复杂度更低的做法

方法二:贪心+二分

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

基于上面的贪心思路我们可以用d[i] 代表长度为i的最长上升子序列的末尾的最小值

用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1]=nums[0]。

我们接着需要证明数组d具有单调性,即证明i<j时,d[i]<d[j],使用反证法,假设存在k<j时,d[k]>d[j],但在长度为j,末尾元素为d[j]的子序列A中,将后j-i个元素减掉,可以得到一个长度为i的子序列B,其末尾元素t1必然小于d[j](因为在子序列A中,t1的位置上在d[j]的后面),而我们假设数组d必须符合表示长度为 i 的最长上升子序列的末尾元素的最小值,此时长度为i的子序列的末尾元素t1<d[j]<d[k],即t1<d[k],所以d[k]不是最小的,与题设相矛盾,因此可以证明其单调性 

最后整个算法流程为:

设当前已求出的最长上升子序列的长度为 len(初始时为 1),从前往后遍历数组 nums,在遍历到 nums[i] 时:

如果 nums[i]>d[len] ,则直接加入到 d 数组末尾,并更新 len=len+1;

否则,在 d 数组中二分查找,找到第一个比 nums[i] 小的数 d[k] ,并更新 d[k+1]=nums[i]。

在这里我们用pos记录,pos记录d数组中小于num[i]的最大值的下标

  • 这一步的意义,在于记录最小序列,代表了一种“最可能性”
  • 例如[0,4,12,2,3,5]中,当判断到 nums[i] = 2时,d={0,4,12},此时根据这个原则,会将4替换为2,即d更新为d={0,2,12},虽然此时不会影响最长升序子序列的长度,但是这一步保存了之后有数字可以和{0,2}组成更长的升序子序列的可能性。例如,该数组的最长升序子序列为 {0,2,3,5},刚好0,2}是这个子序列的前缀,也正是因为我们将 4 替换为了2,所以才保留了这种可能性,否则,遇到3时,将忽略这个数,从而计算错误。
class Solution {
public:
    int lengthOfLIS(vector<int>& num) {
        int n = num.size();
        if(n==0){
            return 0;
        }
        vector<int> d(n+5,0);
        int len = 1;
        d[len]=num[0];
        for(int i=1;i<n;i++)
        {
            if(num[i]>d[len]){
                d[++len]=num[i];
            }
            else{
                int l = 0, r = len+1,pos = 0;
                while(l+1!=r){
                    int mid = (l+r)>>1;
                    if(d[mid]<num[i]){
                        pos = mid;
                        l = mid;
                    }
                    else r = mid;
                }
                d[pos+1]=num[i];
            }
        }
         return len;
    }
};

介绍一下我的blog   sun's blog – 下次你路过,人间已无我。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值