最长递增子序列(LIS)详解:从 dp[i] 到 O(n²) 动态规划

前言

最长递增子序列(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. 手写一遍代码
  2. [1,3,6,7,9,2,4] 手算 dp
  3. 尝试 O(n log n) 优化(耐心排序)

欢迎留言你的 LIS 手算过程!


点赞 + 收藏 + 一键三连,我们下篇见!


参考资料

  • LeetCode #300
  • 《算法导论》
  • CLRS Chapter 15
相关推荐
谢景行^顾2 小时前
数据结构知识掌握
linux·数据结构·算法
ShineWinsu3 小时前
对于数据结构:堆的超详细保姆级解析——下(堆排序以及TOP-K问题)
c语言·数据结构·c++·算法·面试·二叉树·
DuHz3 小时前
基于时频域霍夫变换的汽车雷达互干扰抑制——论文阅读
论文阅读·算法·汽车·毫米波雷达
hetao17338373 小时前
ZYZ28-NOIP模拟赛-Round4 hetao1733837的record
c++·算法
Nebula_g3 小时前
C语言应用实例:解方程(二分查找)
c语言·开发语言·学习·算法·二分查找·基础
少许极端4 小时前
算法奇妙屋(十)-队列+宽搜(BFS)
java·数据结构·算法·bfs·宽度优先·队列
极客学术工坊5 小时前
2023年第十三届MathorCup高校数学建模挑战赛-C题 电商物流网络包裹应急调运与结构优化问题
数学建模·动态规划·启发式算法
想唱rap6 小时前
Linux开发工具(4)
linux·运维·服务器·开发语言·算法