最长递增子序列(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
相关推荐
Xの哲學6 小时前
Linux流量控制: 内核队列的深度剖析
linux·服务器·算法·架构·边缘计算
yaoh.wang7 小时前
力扣(LeetCode) 88: 合并两个有序数组 - 解法思路
python·程序人生·算法·leetcode·面试·职场和发展·双指针
LYFlied7 小时前
【每日算法】 LeetCode 56. 合并区间
前端·算法·leetcode·面试·职场和发展
艾醒8 小时前
大模型原理剖析——多头潜在注意力 (MLA) 详解
算法
艾醒8 小时前
大模型原理剖析——DeepSeek-V3深度解析:671B参数MoE大模型的技术突破与实践
算法
jifengzhiling9 小时前
零极点对消:原理、作用与风险
人工智能·算法
鲨莎分不晴9 小时前
【前沿技术】Offline RL 全解:当强化学习失去“试错”的权利
人工智能·算法·机器学习
XFF不秃头10 小时前
力扣刷题笔记-全排列
c++·笔记·算法·leetcode
菜鸟233号10 小时前
力扣669 修剪二叉搜索树 java实现
java·数据结构·算法·leetcode