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