最长递增子序列(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
相关推荐
Wu_Dylan1 分钟前
智能体系列(二):规划(Planning):从 CoT、ToT 到动态采样与搜索
人工智能·算法
散峰而望7 分钟前
【算法竞赛】栈和 stack
开发语言·数据结构·c++·算法·leetcode·github·推荐算法
知乎的哥廷根数学学派8 分钟前
基于多尺度注意力机制融合连续小波变换与原型网络的滚动轴承小样本故障诊断方法(Pytorch)
网络·人工智能·pytorch·python·深度学习·算法·机器学习
蚊子码农13 分钟前
算法题解记录-208实现Trie前缀树
运维·服务器·算法
2301_8002561114 分钟前
【人工智能引论期末复习】第3章 搜索求解2 - 对抗搜索
人工智能·算法·深度优先
程序猿阿伟22 分钟前
《量子算法开发实战手册:Python全栈能力的落地指南》
python·算法·量子计算
老鼠只爱大米1 小时前
LeetCode算法题详解 438:找到字符串中所有字母异位词
算法·leetcode·双指针·字符串匹配·字母异位词·滑动窗口算法
地平线开发者1 小时前
征程 6 | 平台 QAT 精度一致性问题分析流程
算法·自动驾驶
mjhcsp1 小时前
C++ Manacher 算法:原理、实现与应用全解析
java·c++·算法·manacher 算法
AlenTech1 小时前
198. 打家劫舍 - 力扣(LeetCode)
算法·leetcode·职场和发展