基础题目:300. 最长递增子序列
扩展题目:
- 将三个组排序
- 找出到每个位置为止最长的有效障碍赛跑路线
- 得到山型数组的最少删除次数
- 使数组 K 递增的最少操作次数
一、概述:为什么 LIS 是一种“新的线性 DP”
这两天的题目有一个非常鲜明的共同点:
都是「单数组 + 子序列 + 有序性」问题
它们与之前接触的模型(如 0/1 背包、完全背包、双序列 DP)有本质区别:
- 不是选容量 / 次数
- 不是两个序列之间的匹配
- 而是:在保持原相对顺序的前提下,选出一个“有序的子序列”
这类问题的核心关键词是:
- 子序列(Subsequence)
- 递增 / 不降(有序性约束)
- 最长 / 最少删除(等价转化)
而这一整类问题,几乎都可以归约到一个经典母题:
最长递增子序列(LIS)
二、300. 最长递增子序列(LIS)
从回溯到 O(n²) 线性 DP
思维起点:子序列 = 子集的一种
LIS 本质是一个「选或不选」的问题,可以有两种回溯视角:
- 枚举元素选不选
→ 需要额外维护「上一个选了谁」 - 枚举答案以谁结尾(更适合 DP)
→ 只关心:以 nums[i] 结尾的 LIS 最长是多少
状态定义
1 | dp[i]:以 nums[i] 结尾的最长严格递增子序列长度 |
状态转移
1 | dp[i] = max(dp[j] + 1) (j < i 且 nums[j] < nums[i]) |
复杂度
- 状态数:n
- 转移代价:n
- 时间复杂度:O(n²)
考虑优化时间复杂度。
贪心 + 二分:从 O(n²) 到 O(n log n)
当 n 上到 10⁵ 时,O(n²) 就不够用了,需要进一步优化。
核心技巧只有一句话:
将 dp 的「状态」和「状态值」进行交
关键构造:g 数组
我们不再显式记录「每个位置的 LIS 长度」,而是维护一个数组:
1 | g[k]:长度为 k+1 的递增子序列,其末尾元素的最小可能值 |
它有两个重要性质:
g始终是单调递增的g.size()就是当前 LIS 的长度
遍历过程(贪心本质)
对每个元素 x = nums[i]:
- 在
g中二分查找:- 严格递增 LIS:找第一个
>= x的位置
- 严格递增 LIS:找第一个
- 分两种情况:
- 找不到(位置 == g.size)
→ 说明可以扩展长度,g.add(x) - 找到了
→ 用x替换该位置,g[pos] = x
- 找不到(位置 == g.size)
为什么可以替换+为什么要替换
因为:
- 我们只关心 在固定长度下,末尾元素越小越好
- 更小的末尾元素,能给后续元素留下更大的扩展空间
这就是贪心正确性的核心。
时间复杂度:O(n log n)
三、LIS 的“模型迁移”:扩展题统一视角
2826. 将三个组排序
核心转化:
- 不要求严格递增
- 只要求 非递减子序列
问题等价于:
1 | 最少操作数 = n - 最长非递减子序列长度 |
实现细节:
- 二分时找 第一个 > x 的位置
1964. 有效障碍赛跑路线
题目要求的是:
到每个位置为止的最长非递减子序列长度
关键点:
g数组的维护方式不变- 每次插入 / 替换后:当前
g.size()就是答案,因为遍历nums的过程,每个数都对应一次操作。
本质是 LIS 贪心过程的“在线版本”
1671. 得到山型数组的最少删除次数
标准「山型数组」模型:
- 枚举
i作为峰顶 - 左边:最长严格递增子序列
- 右边:最长严格递减子序列
处理技巧:
- 右侧「严格递减」
→ 倒序遍历,等价于再做一次 LIS
答案计算:
1 | n - (len_left + len_right - 1) |
注意边界:
- 峰顶左右必须至少有一个元素 →
len_left >= 2 && len_right >= 2
2111. 使数组 K 递增的最少操作次数
核心拆解:
- 按下标
i % k分成 k 组 - 每一组 互不影响
- 对每一组:
- 求最长非递减子序列长度
最终答案:
1 | 总长度 - 各组 LNDS 之和 |
这是一个非常经典的:
“全局问题 → 独立子问题 → LIS 统一求解”
四、方法论总结
子序列 + 最少操作 = 最长子序列
只要题目满足:
- 只能删除 / 修改
- 不改变相对顺序
几乎都可以转化为:
1 | 最少操作 = n - 最长满足条件的子序列长度 |
严格递增 vs 非递减(二分差异)
| 子序列类型 | 二分查找条件 |
|---|---|
| 严格递增 LIS | 找第一个 >= x |
| 非递减 LNDS | 找第一个 > x |
这是 LIS 题中最容易写错的点
为什么 LIS 属于线性 DP
- 本质仍是 DP
- 只是通过:
- 贪心
- 有序结构
- 二分查找
- 将状态转移从
O(n)优化成O(log n)
这个优化的关键技巧只有一句话:
交换 dp 的“状态”和“状态值”
五、总结
300. 最长递增子序列(LIS)是一个“母题”:
它不是一题,而是一整类题型的统一入口
一旦 LIS 的 O(n²) 与 O(n log n) 两套思路真正吃透:
山型数组
非递减序列
分组 LIS
最少删除 / 修改问题
都可以自然转化、快速识别
相关代码
本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。