题目描述
给你一个整数数组 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) - 输入:
nums = [0,1,0,3,2,3]→ 输出:4 - 输入:
nums = [7,7,7,7,7,7,7]→ 输出:1
解题思路总览
| 方法 | 思路 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 动态规划 | dp[i] 表示以 nums[i] 结尾的最长递增子序列长度,状态转移 dp[i] = max(dp[j]+1) |
O(n^2) | O(n) |
| 二分查找 | 维护一个有序数组 tail,用二分查找更新,最长递增子序列长度即为 tail 的大小 |
O(n log n) | O(n) |
本题采用动态规划方法,并给出二分查找的进阶解法。
完整代码
动态规划版本:
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> dp(n, 0);
int ans = INT_MIN;
for (int i = 0; i < n; i++) {
dp[i] = 1;
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;
}
};
二分查找版本(进阶 O(n log n)):
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> tail;
for (int x : nums) {
auto it = lower_bound(tail.begin(), tail.end(), x);
if (it == tail.end()) {
tail.push_back(x);
} else {
*it = x;
}
}
return tail.size();
}
};
算法流程图
输入: nums = [10, 9, 2, 5, 3, 7, 101, 18]
动态规划过程:
初始化:
n = 8
dp[0...7] = 0
ans = -∞
i = 0, nums[0] = 10:
dp[0] = 1
ans = 1
i = 1, nums[1] = 9:
j = 0: 10 < 9? 否
dp[1] = 1
ans = 1
i = 2, nums[2] = 2:
j = 0: 10 < 2? 否
j = 1: 9 < 2? 否
dp[2] = 1
ans = 1
i = 3, nums[3] = 5:
j = 0: 10 < 5? 否
j = 1: 9 < 5? 否
j = 2: 2 < 5? 是
dp[3] = max(1, dp[2]+1) = 2
dp[3] = 2
ans = 2
i = 4, nums[4] = 3:
j = 0: 10 < 3? 否
j = 1: 9 < 3? 否
j = 2: 2 < 3? 是
dp[4] = max(1, dp[2]+1) = 2
dp[4] = 2
ans = 2
i = 5, nums[5] = 7:
j = 0: 10 < 7? 否
j = 1: 9 < 7? 否
j = 2: 2 < 7? 是
dp[5] = max(1, dp[2]+1) = 2
j = 3: 5 < 7? 是
dp[5] = max(2, dp[3]+1) = 3
j = 4: 3 < 7? 是
dp[5] = max(3, dp[4]+1) = 3
dp[5] = 3
ans = 3
i = 6, nums[6] = 101:
... (所有前面的数都 < 101)
dp[6] = 4
ans = 4
i = 7, nums[7] = 18:
... (10,9,2,5,3,7 < 18,但 18 < 101)
dp[7] = 4
ans = 4
最终 ans = 4
输出: 4
逐行解析
动态规划版本逐行解析:
cpp
int n = nums.size();
含义: 记录数组长度。
cpp
if (n == 0) return 0;
含义: 空数组没有递增子序列,直接返回 0。
cpp
vector<int> dp(n, 0);
含义: dp[i] 表示以 nums[i] 结尾的最长递增子序列长度。初始化为 0,后续会被更新。
cpp
int ans = INT_MIN;
含义: 记录全局最长递增子序列长度,初始化为最小值。
cpp
for (int i = 0; i < n; i++)
含义: 遍历数组,以每个位置作为递增子序列的结尾。
cpp
dp[i] = 1;
含义: 初始化 dp[i] = 1,表示以 nums[i] 结尾的递增子序列至少包含它自己。
cpp
for (int j = 0; j < i; j++)
含义: 枚举所有在 i 之前的元素 j,尝试将其接到以 nums[j] 结尾的递增子序列后面。
cpp
if (nums[j] < nums[i])
含义: 只有当 nums[j] 小于 nums[i] 时,才能将 nums[i] 接在以 nums[j] 结尾的递增子序列后面(严格递增)。
cpp
dp[i] = max(dp[i], dp[j] + 1);
含义: 状态转移方程。如果将 nums[i] 接在 nums[j] 后面,新的递增子序列长度为 dp[j] + 1,取较大值更新 dp[i]。
cpp
ans = max(ans, dp[i]);
含义: 更新全局最长长度。
cpp
return ans;
含义: 返回最长递增子序列的长度。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 动态规划 | O(n^2) | O(n) |
| 二分查找 | O(n log n) | O(n) |
动态规划说明: 外层循环 n 次,内层循环最多 n 次,时间复杂度 O(n^2)。
二分查找说明: 遍历数组 n 次,每次二分查找 O(log n),总时间复杂度 O(n log n)。
面试追问 FAQ
| 问题 | 答案 |
|---|---|
动态规划中 dp[i] 的含义是什么? |
dp[i] 表示以 nums[i] 结尾的最长递增子序列长度。注意必须以 nums[i] 结尾 |
为什么不直接返回 dp[n-1]? |
因为最长递增子序列不一定以最后一个元素结尾,例如 [1, 2, 3, 0] 中最长递增子序列以索引 2 结尾而不是 3 |
二分查找版本中 tail 数组的含义是什么? |
tail[i] 表示长度为 i+1 的递增子序列的最小结尾元素。tail 数组是严格递增的 |
二分查找版本为什么用 lower_bound 而不是 upper_bound? |
因为要找的是「不小于 x」的位置,对于严格递增子序列要用 lower_bound(找第一个 >= x 的位置) |
| 如何输出具体的递增子序列? | 动态规划版本需要额外记录每个状态是从哪个 j 转移来的,然后从后往前回溯 |
| 进阶:如何修改代码求最长非递减子序列? | 将 if (nums[j] < nums[i]) 改为 if (nums[j] <= nums[i]),允许相等元素 |
| 进阶:如何求最长递减子序列? | 将数组反转后求最长递增子序列,或将比较条件改为 > |
相关题目
| 题号 | 题目 | 难度 | 核心思路 |
|---|---|---|---|
| 300 | 最长递增子序列 | 中等 | 动态规划/二分查找 |
| 354 | 俄罗斯套娃信封问题 | 困难 | 排序 + 最长递增子序列 |
| 673 | 最长递增子序列的个数 | 中等 | 动态规划 + 计数 |
| 491 | 递增子序列 | 中等 | DFS + 去重 |
总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 动态规划:以每个元素作为递增子序列的结尾,求最长长度 |
| 状态定义 | dp[i] = 以 nums[i] 结尾的最长递增子序列长度 |
| 状态转移 | dp[i] = max(dp[j] + 1),其中 j < i 且 nums[j] < nums[i] |
| 初始化 | dp[i] = 1(每个元素自身构成长度为 1 的递增子序列) |
| 结果 | 返回 dp 数组中的最大值 |