前言
最长递增子序列(Longest Increasing Subsequence, LIS) 是动态规划中的经典问题,经常出现在:
- 笔试 / 面试(字节、阿里、腾讯高频)
- LeetCode #300
- 算法竞赛
今天用 图文结合的方式 ,从 状态定义 → 转移方程 → 代码实现 → 复杂度分析,一步步带你彻底掌握!
1. 什么是 LIS?
在一个数组中,找出最长的严格递增的子序列的长度。
示例:
cpp
nums = [10, 9, 2, 5, 3, 7, 101, 18]
可能的递增子序列:
[2, 5, 7, 101]→ 长度 4[2, 3, 7, 18]→ 长度 4[10, 101]→ 长度 2
答案:4
2. 动态规划的核心:dp[i] 是什么?
dp[i]表示:以nums[i]作为结尾的最长严格递增子序列的长度。
| 索引 | 值 | dp[i] | 含义 |
|---|---|---|---|
| 0 | 10 | 1 | 只有 [10] |
| 1 | 9 | 1 | 只有 [9] |
| 2 | 2 | 1 | 只有 [2] |
| 3 | 5 | 2 | [2,5] |
| 4 | 3 | 2 | [2,3] |
| 5 | 7 | 3 | [2,5,7] 或 [2,3,7] |
| 6 | 101 | 4 | [2,5,7,101] |
| 7 | 18 | 4 | [2,3,7,18] |
最终答案 =
max(dp[i])= 4
3. 状态转移图(可视化理解)
我们用 有向图 表示状态转移:
2
dp=1 5
dp=2 3
dp=2 7
dp=3 101
dp=4 18
dp=4
每条边表示一次状态转移:
dp[i] = dp[j] + 1
4. 状态转移方程
cpp
for i from 1 to n-1:
for j from 0 to i-1:
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
初始状态:
cpp
dp[i] = 1 // 每个元素自己是一个长度为 1 的序列
5. 完整代码(C++)
cpp
#include <vector>
#include <algorithm>
using namespace std;
int longestIncreasingSubsequence(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> dp(n, 1); // 关键:初始化为 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;
}
6. 逐步推导(手算过程)
| i | nums[i] | j 候选 | 转移 | dp[i] | maxLen |
|---|---|---|---|---|---|
| 0 | 10 | --- | 初始 | 1 | 1 |
| 1 | 9 | --- | 初始 | 1 | 1 |
| 2 | 2 | --- | 初始 | 1 | 1 |
| 3 | 5 | j=2 | 1→2 | 2 | 2 |
| 4 | 3 | j=2 | 1→2 | 2 | 2 |
| 5 | 7 | j=3,4 | 2→3 | 3 | 3 |
| 6 | 101 | 所有 | 3→4 | 4 | 4 |
| 7 | 18 | j=5 | 3→4 | 4 | 4 |
7. 复杂度分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | O(n²) ------ 双重循环 |
| 空间复杂度 | O(n) ------ dp 数组 |
8. 进阶:如何还原 LIS 路径?
cpp
vector<int> prev(n, -1);
int maxIdx = 0;
// 在状态转移时记录前驱
if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j;
if (dp[i] > dp[maxIdx]) maxIdx = i;
}
// 回溯路径
vector<int> lis;
for (int i = maxIdx; i != -1; i = prev[i]) {
lis.push_back(nums[i]);
}
reverse(lis.begin(), lis.end());
输出:[2, 5, 7, 101]
9. 记忆口诀(三句话)
1. dp[i] → 以 i 结尾
2. 初始 1 → 自己就是 1
3. 小 → 大 → 加 1
10. 常见变种
| 题目 | 变化点 |
|---|---|
| 最长连续递增子序列 | 必须连续 |
| 俄罗斯套娃信封 | 二维 LIS |
| 最长递增子序列的个数 | 计数 DP |
结语
LIS 是动态规划的"入门神题",掌握它,你就掌握了:
- 状态定义
- 状态转移
- 子问题分解
下一步建议:
- 手写一遍代码
- 用
[1,3,6,7,9,2,4]手算dp表 - 尝试 O(n log n) 优化(耐心排序)
欢迎留言你的 LIS 手算过程!
点赞 + 收藏 + 一键三连,我们下篇见!
参考资料:
- LeetCode #300
- 《算法导论》
- CLRS Chapter 15