LeetCode 300:最长递增子序列

LeetCode 300:最长递增子序列

1. 题目核心

给定整数数组 nums,求其中最长严格递增子序列 的长度。子序列不要求连续,但必须保持原数组中的相对顺序。

例如:

复制代码
nums = [10,9,2,5,3,7,101,18]

一种最长递增子序列:

复制代码
2 → 3 → 7 → 101

长度为:

复制代码
4

注意:

复制代码
子数组:必须连续
子序列:可以跳过中间元素

2. 思路一:DFS / 枚举

对于每个数字,都可以考虑:

复制代码
选择它
不选择它

然后枚举所有可能的子序列,判断是否严格递增并记录最大长度。这种方法会产生大量重复情况,最坏接近指数级复杂度,不适合作为主解。

3. 思路二:动态规划 DP

3.1 DP 状态定义

这题最关键的是不能只记录"当前最长的那一条序列",因为当前数字可能接不上最长序列,却可以接在另一条较短但结尾更小的序列后面,并在以后反超。

因此定义:

复制代码
dp[i]
=
以 nums[i] 作为最后一个数字时,
最长递增子序列的长度

例如:

复制代码
nums = [1,5,2,3,4]

可能得到:

复制代码
nums:1  5  2  3  4
dp:  1  2  2  3  4

其中:

复制代码
dp[1] = 2 → 1,5
dp[2] = 2 → 1,2

虽然两者长度相同,但是 1,2 的结尾更小,所以后面可以继续接:

复制代码
1 → 2 → 3 → 4

最终超过原来的 1 → 5。

3.2 状态转移

现在处理 nums[i],检查它前面的所有位置:

复制代码
j = 0 ~ i-1

如果:

复制代码
nums[j] < nums[i]

说明 nums[i] 可以接在以 nums[j] 结尾的递增子序列之后。

原来的长度:

复制代码
dp[j]

加上当前数字:

复制代码
dp[j] + 1

因为可能有多个 j 都满足条件,所以选择最大的:

复制代码
dp[i] = max(dp[i], dp[j] + 1);

完整关系:

复制代码
对于所有 j < i:

如果 nums[j] < nums[i]
    dp[i] = max(dp[i], dp[j] + 1)

3.3 为什么初始化全部为 1

任何一个数字单独拿出来,都能形成长度为 1 的递增子序列:

复制代码
vector<int> dp(n, 1);

例如:

复制代码
[7]

本身就是长度为 1 的递增子序列。

3.4 为什么不能只和前一个数字比较

错误思路:

复制代码
if (nums[i] > nums[i - 1])
    dp[i] = dp[i - 1] + 1;
else
    dp[i] = 1;

这算的是连续递增子数组 ,不是递增子序列。

例如:

复制代码
nums = [0,1,0,3,2,3]

最长递增子序列:

复制代码
0 → 1 → 2 → 3

长度为 4。

其中 2 并不比它前面的 3 大:

复制代码
3 > 2

但它可以跳过 3,接在:

复制代码
0 → 1

后面。

3.5 示例完整推导

复制代码
nums = [0,1,0,3,2,3]

初始化:

复制代码
dp = [1,1,1,1,1,1]

处理 1:

复制代码
0 < 1
dp[1] = dp[0] + 1 = 2

dp = [1,2,1,1,1,1]

处理第二个 0:

复制代码
前面没有比0小的数字
dp[2] = 1

dp = [1,2,1,1,1,1]

处理 3:

复制代码
0 < 3 → 1+1=2
1 < 3 → 2+1=3
0 < 3 → 1+1=2

dp[3] = 3

dp = [1,2,1,3,1,1]

处理 2:

复制代码
0 < 2 → 2
1 < 2 → 3
0 < 2 → 2
3 > 2 → 不能接

dp[4] = 3

dp = [1,2,1,3,3,1]

最后处理 3:

复制代码
2 < 3
dp[4] + 1 = 4

得到:

复制代码
dp = [1,2,1,3,3,4]

答案:

复制代码
4

3.6 为什么最终不能直接返回 dp[n-1]

因为:

复制代码
dp[i]

表示的是:

必须以 nums[i] 结尾 的最长递增子序列。

整个数组的最长递增子序列不一定以最后一个数字结束。

例如:

复制代码
nums = [1,2,3,0]
dp   = [1,2,3,1]

答案应该是:

复制代码
3

而不是:

复制代码
dp[3] = 1

所以需要:

复制代码
maxLen = max(maxLen, dp[i]);

4. C++:DP 解法

复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();

        // dp[i]:以 nums[i] 结尾的最长递增子序列长度
        vector<int> dp(n, 1);

        int maxLen = 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);
                }
            }

            maxLen = max(maxLen, dp[i]);
        }

        return maxLen;
    }
};

复杂度:

复制代码
时间复杂度:O(n²)
空间复杂度:O(n)

5. 最优思路:贪心 + 二分查找

DP 的问题是每个 nums[i] 都要检查前面的所有数字,所以需要 O(n²)。

可以维护一个数组:

复制代码
tails

其中:

复制代码
tails[len-1]
=
长度为 len 的递增子序列中,
能够得到的最小结尾值

例如处理:

复制代码
nums = [10,9,2,5,3,7,101,18]

过程大致为:

复制代码
10       → [10]
9        → [9]
2        → [2]
5        → [2,5]
3        → [2,3]
7        → [2,3,7]
101      → [2,3,7,101]
18       → [2,3,7,18]

最终:

复制代码
tails.size() = 4

所以最长递增子序列长度为 4。

注意:

tails 不一定是真实的最长递增子序列,它主要用来记录"同样长度下尽可能小的结尾"。

5.1 为什么结尾越小越好

例如已经有:

复制代码
1 → 5

又遇到:

复制代码
2

虽然:

复制代码
1 → 2

长度仍然是 2,但结尾从 5 变成 2。

显然:

复制代码
以2结尾

比:

复制代码
以5结尾

更容易继续接 3、4 等数字。

所以我们始终希望:

同样长度的递增子序列,结尾越小越好。

5.2 每个数字怎么处理

对于当前数字 x:

  • 如果 x 比 tails 所有数字都大,说明可以延长最长递增子序列,直接放到末尾;
  • 否则找到 tails 中第一个大于等于 x 的位置 ,用 x 替换它。

例如:

复制代码
tails = [2,5]
当前 x = 3

找到第一个:

复制代码
>= 3

的是 5,替换:

复制代码
[2,5]
↓
[2,3]

长度没变,但结尾变小了,为后面留下更多可能。

5.3 为什么找"第一个 >= x"

题目要求的是严格递增 。

如果:

复制代码
tails = [2,3,7]
x = 3

不能把另一个 3 接到后面形成:

复制代码
2 → 3 → 3

因为不是严格递增。

所以要找到第一个:

复制代码
>= x

的位置进行替换。

C++ 正好可以使用:

复制代码
lower_bound()

6. C++:最优解 O(n log n)

复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> tails;

        for (int x : nums) {
            auto it = lower_bound(tails.begin(), tails.end(), x);

            if (it == tails.end()) {
                tails.push_back(x);
            } else {
                *it = x;
            }
        }

        return tails.size();
    }
};

复杂度:

复制代码
时间复杂度:O(n log n)
空间复杂度:O(n)

7. Java:最优解

复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] tails = new int[nums.length];
        int size = 0;

        for (int x : nums) {
            int left = 0;
            int right = size;

            while (left < right) {
                int mid = left + (right - left) / 2;

                if (tails[mid] < x) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }

            tails[left] = x;

            if (left == size) {
                size++;
            }
        }

        return size;
    }
}

8. Java 语法解释

8.1 创建数组

复制代码
int[] tails = new int[nums.length];

创建一个和 nums 一样长的整数数组。

8.2 增强 for 循环

复制代码
for (int x : nums)

表示依次取出数组中的每个数字,对应 C++:

复制代码
for (int x : nums)

8.3 二分查找范围

复制代码
int left = 0;
int right = size;

查找范围是:

复制代码
[0, size)

寻找第一个:

复制代码
tails[index] >= x

的位置。

8.4 二分判断

复制代码
if (tails[mid] < x) {
    left = mid + 1;
} else {
    right = mid;
}

如果:

复制代码
tails[mid] < x

说明当前位置太小,答案一定在右边。

否则当前 mid 有可能就是第一个 >= x 的位置,因此:

复制代码
right = mid;

8.5 left == size

如果二分结束:

复制代码
left == size

说明整个 tails 中都没有 >= x 的数字,也就是:

复制代码
x 比所有结尾都大

可以延长最长递增子序列:

复制代码
size++;

9. Python:最优解

复制代码
from bisect import bisect_leftclass Solution:    def lengthOfLIS(self, nums):        tails = []        for x in nums:            index = bisect_left(tails, x)            if index == len(tails):                tails.append(x)            else:                tails[index] = x        return len(tails)

10. Python 语法解释

10.1 bisect_left

复制代码
from bisect import bisect_left

导入 Python 自带的二分查找函数。

复制代码
index = bisect_left(tails, x)

表示:

在已经有序的 tails 中寻找第一个 >= x 的位置。

基本对应 C++:

复制代码
lower_bound(tails.begin(), tails.end(), x)

10.2 空列表

复制代码
tails = []

创建一个空列表。

10.3 append

复制代码
tails.append(x)

把 x 添加到列表末尾,对应 C++:

复制代码
tails.push_back(x);

10.4 len

复制代码
len(tails)

表示列表长度。

11. 两种主要解法对比

方法 时间复杂度 空间复杂度 特点
DFS / 枚举 指数级 较高 不推荐
动态规划 O(n²) O(n) 最容易理解,重点掌握
贪心 + 二分 O(n log n) O(n) 最优常规解法

12. 核心记忆

12.1 DP 定义

复制代码
dp[i]
=
以 nums[i] 结尾的
最长递增子序列长度

12.2 DP 转移

对于所有:

复制代码
j < i

如果:

复制代码
nums[j] < nums[i]

则:

复制代码
dp[i] = max(dp[i], dp[j] + 1);

本质是:

当前数字去前面寻找所有比自己小的数字,从它们对应的递增子序列中选择最长的一条,再把自己接上去。

12.3 为什么不能只保存一条最长序列

例如:

复制代码
1 → 5
1 → 2

虽然长度都是 2,但第二条结尾更小,未来更容易继续:

复制代码
1 → 2 → 3 → 4

所以 DP 要给每个位置分别保存一个状态,不能只记录一个全局最长序列。

12.4 最优解核心

维护:

复制代码
tails[len-1]
=
长度为 len 的递增子序列中
最小的结尾值

当前数字 x:

复制代码
比所有结尾都大
→ 添加到 tails 末尾

否则
→ 找第一个 >= x 的位置并替换

最后:

复制代码
tails 的长度 = LIS 长度

一句话记忆:

DP 是"当前数字去前面找所有能接的序列";二分优化则是"只保留每种长度下最小的结尾",因为结尾越小,未来越容易继续增长。

相关推荐
hetao17338371 小时前
2026-10-01~02 hetao1733837 的刷题记录
c++·算法
小虎牙^O^1 小时前
模糊数学综合评价:从模糊数学到建模评价类模型
人工智能·算法
随意起个昵称2 小时前
【单调队列优化DP】U301134 绿色通道
算法·动态规划
Nil2082 小时前
leetcode 739每日温度
算法·leetcode·职场和发展
lvwangshu2 小时前
势能分析(摊还分析)
数据结构·数学·算法
盘古开天16663 小时前
PPO算法代码实战(三):PyTorch从零实现PPO求解CartPole
人工智能·pytorch·算法
yyy(十一月限定版)3 小时前
CF2138A Cake Assignment
算法
盘古开天16663 小时前
PPO算法原理详解(下):Clip机制深入剖析与实践指南
人工智能·算法·机器学习
All for pursuit.3 小时前
【贪心-4】581.最短无序连续子数组
数据结构·c++·算法·leetcode