最长递增子序列(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唱着歌15 小时前
LeetCode 1019.链表中的下一个更大节点
算法·leetcode·链表
凌波粒16 小时前
LeetCode--404.左叶子之和(二叉树)
算法·leetcode·职场和发展
paeamecium16 小时前
【PAT甲级真题】- A+B in Hogwarts
c++·算法·pat考试·pat
青山师16 小时前
二叉树与BST深度解析:遍历算法与平衡策略
数据结构·算法·面试·二叉树·算法与数据结构·java面试·数据结构与算法分析
绝知此事16 小时前
【算法突围 03】核心算法思想:分治/递归/动态规划与 LeetCode 高频真题解析
算法·leetcode·面试·动态规划
AI科技星16 小时前
第二章 平行素数对网格:矩形→等腰梯形拓扑变换(完整公理终稿)
c语言·开发语言·线性代数·算法·量子计算·agi
AI视觉网奇17 小时前
blender bpy对齐物体
算法
吃好睡好便好17 小时前
在Matlab中绘制阶梯图
开发语言·人工智能·学习·算法·机器学习·matlab
Deep-w17 小时前
【MATLAB】基于 MATLAB 的离网光伏储能微电网容量优化仿真研究
开发语言·算法·matlab