最长递增子序列(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
相关推荐
董董灿是个攻城狮10 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
AI软著研究员17 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish17 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱18 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx2 天前
CART决策树基本原理
算法·机器学习
Wect2 天前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript