力扣300-最长递增子序列

题目链接:300. 最长递增子序列 - 力扣(LeetCode)

给你一个整数数组 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
  • -104 <= nums[i] <= 104

进阶:

  • 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

方法一:动态规划

从一般情况考虑,求dp[i](设dp[i]表示以nums[i]结尾的最长递增子序列长度),只需要找出nums[i]之前比其小的nums[j],那么dp[i]就等于dp[j] + 1

由于要求最大,所以在遍历的时候用max更新最大的即可

递推公式已经出来了,接着考虑初始化。显然每个元素都初始化为1,因为一个数字也可以构成一个子序列

最后,用max更新最大的dp[i]即可

Java

java 复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        int ans = dp[0];
        for(int i = 1; i < n; i++) {
            for(int j = i - 1; j >= 0; j--) {
                if(nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

C++

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int ans=0;
        int n=nums.size();
        if (n<=1) return n;
        vector<int>dp(n,1);
        for(int i=1;i<n;i++) {
            for(int j=0;j<i;j++) {
                if(nums[j]<nums[i]) {
                    dp[i]=max(dp[i],dp[j]+1);//这里不是比较,而是更新最大值
                }
            }
            ans=max(ans,dp[i]);//这里同理,也是在更新最大值
        }
        return ans;
    }
};

Go

Go 复制代码
func lengthOfLIS(nums []int) int {
    dp:=make([]int,len(nums))
    for i:= range dp {
        dp[i]=1
    }
    ans:=dp[0]
    for i:=1;i<len(nums);i++ {
        for j:=0;j<i;j++ {
            if nums[j]<nums[i] {
                dp[i]=max(dp[i],dp[j]+1)
            }
        }
        ans=max(ans,dp[i])
    }
    return ans
}

时间复杂度为O(N^2),考虑进行优化

方法二:贪心+二分

假设nums为[1,2,99999,3,4],当遍历完前两个元素,也就是1和2之后,此时最长递增子数组(以下简称LIS)为[1,2]

接下来你会选择99999加入LIS中,还是选择后面的3加入到LIS中?

显然应该选择更小的3,如果选择99999,那么就会错过3后面的4

选择了3,就能得到正确的LIS,即[1,2,3,4]

这是一种贪心思想:为了使数组递增的长度更大,我们应该让它增长的速度尽可能慢,这样就尽可能避免了错过后面单增的可能,表示一种"最可能性"

落实到具体数组中,我们的选择策略尽管依然是选择比LIS中元素更大的元素,但是新增了一个条件是:要在这些更大的元素中,选择最小的那个。但问题来了,我们在遍历的时候,怎么知道后面的元素是否有更小的?

我们可以记录当前LIS的长度len,用一个数组d记录LIS的末尾元素(也就是最大值)的最小值,即d[len]表示长度为len的LIS的末尾元素的最小值

显然d[i]随i的增大而增大。因为如果 d[j]≥d[i] 且 j<i,考虑从长度为i的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 d[i](因为第j个元素不是最后一个元素,就必然比最后一个元素要小),也就小于 d[j],那么就找到了一个长度为 j 的最长上升子序列,并且末尾元素比d[j]小,而d[j]的定义是长度为j的LIS的末尾元素的最小值,这与定义相矛盾,因此单调性得证

如果遍历的元素nums[i]d[len]更小,说明存在一种"增长速度更慢"的可能,是哪个子数组的增长速度可能更慢?为了回答这个问题,我们需要在d中找到从右往左数第一个比nums[i]更小的元素,由于d有序,可以用二分。假设其位置为k,那么将d[k+1]替换为nums[i]后,d[1...k+1]的增长速度就比没替换之前更慢,长度为k+1的LIS后续变得更长的可能性就变高了

如果nums[i]d[len]更大,那就意味着nums[i]可以成为长度为len+1的LIS的末尾元素。直接让len自增1,并且让d[len+1]nums[i]即可,因为此时len的长度发生了变化,只有一个长度为len+1的LIS,所以末尾元素的最小值只能是新加入的nums[i]

Java

java 复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n == 0) return 0;
        int[] d = new int[n + 1]; //d[len]表示长度为len的LIS末尾元素的最小值
        int len = 1; //LIS的长度,初始时只有第一个元素,因此为1
        d[len] = nums[0]; //初始时,len为1,LIS为第一个元素自己构成的数组,所以最小值就是它本身

        for(int i = 1; i < n; i++) {
            if(nums[i] > d[len]) {
                //长度为len的、末尾元素最小的LIS的最大值遇到了比它更大的元素,应该将其加入该LIS当中
                d[++len] = nums[i]; //原本长度为len,现在长度为len+1,d[len+1]表示的是长度为len+1的LIS末尾元素的最小值
            } else {
                //二分法找到d中从右往左数第一个比nums[i]小的位置pos,让d[pos+1]为nums[i]
                //如果d中所有元素都比nums[i]要大,更新d[1],因为以nums[i]结尾的、长度为1的LIS有最小的末尾元素,所以pos初始值为0
                int pos = 0;
                int l = 1, r = len; //d[0]没有实际含义,所以左端点为1,右端点为len
                while(l <= r) {
                    int mid = (l + r) >> 1;
                    if(d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
            }
        }
        return len;
    }
}
相关推荐
小李飞刀李寻欢3 小时前
DeepSeek V3 版本模型结构分析
算法·大模型·deepseek
某不知名網友3 小时前
C++ 七大排序算法完整讲解
java·算法·排序算法
:-)3 小时前
算法-希尔排序
数据结构·算法·排序算法
得物技术3 小时前
得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理
人工智能·算法·架构
Lugas3 小时前
为啥说男生找对象尽量在25岁前找到?
算法
MrZhao4004 小时前
从能跑到可用:一个 Agent Harness 还差哪些工程闭环?
算法
QN1幻化引擎4 小时前
Gravity-Anchored Cognitive Field Architecture: The DalinX V8/V10 Implementation
java·前端·算法
学计算机的计算基5 小时前
LeetCode 图论四题精讲:BFS、拓扑排序、Trie 树的模板与优化
java·笔记·算法
浩瀚地学5 小时前
【面试算法笔记】0202-链表-基本功能实现
java·经验分享·笔记·算法·面试