2407. 最长递增子序列 II
考点
- LIS
- 线段树
思路
从暴力 DP 到值域线段树优化
1. 问题描述
给定一个整数数组 nums 和一个整数
k,要求求出一个严格递增子序列的最大长度,并满足相邻元素差值约束:
\[
0 < nums[i] - nums[j] \le k,\quad j < i
\]
2. 暴力动态规划解法
2.1 状态定义
定义状态: \[ f[i] = \text{以 } nums[i] \text{ 结尾的最长合法递增子序列长度} \]
2.2 状态转移
枚举前一个位置 \(j < i\),若满足: \[ 0 < nums[i] - nums[j] \le k \] 则可以从 \(j\) 转移到 \(i\): \[ f[i] = \max(f[i], f[j] + 1) \] 最终答案为: \[ \max_{1 \le i \le n} f[i] \]
2.3 暴力 DP 代码(\(O(n^2)\))
下面代码直接实现了上述转移逻辑:
1 | class Solution { |
2.4 复杂度分析
时间复杂度: \[ O(n^2) \]
当 \(n \le 10^5\) 时必然超时
3. 转移形式的等价变换
将转移条件按“值域”重写: \[ f[i] = 1 + \max \{ f[j] \mid nums[j] \in [nums[i]-k,\ nums[i]-1] \} \] 关键观察:
- 转移与 下标顺序无关
- 只关心 值域区间内的最大 DP 值
这意味着可以将“枚举所有 \(j\)”转化为:
在值域区间 \([x-k, x-1]\) 内查询最大值
4. 值域线段树建模
4.1 维护对象
定义: \[ dp[v] = \max\{\text{所有以数值 } v \text{ 结尾的子序列长度}\} \] 线段树节点维护:
- 区间内所有 \(dp[v]\) 的最大值
4.2 操作定义
对于每个元素 \(x = nums[i]\):
区间查询 \[ best = \max dp[v],\quad v \in [x-k, x-1] \]
单点更新 \[ dp[x] = \max(dp[x], best + 1) \]
5. 动态开点线段树实现
5.1 设计要点
- 使用节点池,避免
new - 空节点表示该区间的 DP 全为 0
- 只在访问子节点时才创建(动态开点)
节点结构:
1 | struct node { |
6. 线段树优化后的 AC 代码(\(O(n \log V)\))
下面是基于上述建模的 完整 AC 实现,严格对应前面的数学推导:
1 | class Solution { |
7. 复杂度对比总结
| 方法 | 时间复杂度 | 核心瓶颈 |
|---|---|---|
| 暴力 DP | \(O(n^2)\) | 枚举所有前驱 |
| 线段树优化 | \(O(n \log V)\) | 区间最大值查询 |
其中 \(V\) 为值域大小。
8. 总结
- 本题的核心在于: 将“枚举前驱下标”转化为“值域区间最大值查询”
- 动态规划转移式直接导出了线段树建模
- 动态开点线段树使算法在大值域下仍然可行
- 这是一个典型的 DP + 数据结构优化 题目范例