446. 等差数列划分 II - 子序列
考点
- 线性DP
思路
1. 问题描述
给定一个整数数组 \[ \texttt{nums} = [a_0, a_1, \dots, a_{n-1}] \] 需要统计其中 长度至少为 3 的等差子序列 的个数。
注意:
- 子序列不要求连续;
- 等差数列要求相邻元素差值相等;
- \(n \le 1000\),元素范围为 32 位有符号整数。
2. 问题建模思路
2.1 为什么不能直接枚举?
若直接枚举所有子序列:
- 子序列数量为 \(2^n\),完全不可行;
- 即使只枚举三元组或更长序列,也无法有效保证等差性。
因此需要利用 动态规划,逐步“构造”等差子序列。
2.2 建模的关键观察
设某个等差子序列的最后两个元素下标为 \(j < i\),差值为 \[ d = a_i - a_j \] 若此前存在一个以 \(a_j\) 结尾、差值同为 \(d\) 的等差子序列,那么把 \(a_i\) 接在其末尾后,仍然是等差子序列。
因此,等差子序列可以通过“按差值分类的状态转移”来构造。
3. 状态设计
3.1 状态定义
定义状态: \[ f[i][d] = \text{以 } a_i \text{ 结尾,公差为 } d \text{ 的等差子序列个数(长度 ≥ 2)} \] 说明:
- 状态只统计 长度 ≥ 2 的等差子序列;
- 长度为 2 的序列是后续构造合法答案(长度 ≥ 3)的“种子”;
- 最终答案并不直接来自 \(f\),而是在状态转移过程中累加。
实现上,使用:
1 | vector<unordered_map<long long, long long>> f; |
其中:
- 外层下标是结尾位置
i; - 内层
unordered_map的 key 是差值d,value 是计数。
3.2 为什么不直接存长度 ≥ 3?
因为:
- 长度为 3 的等差子序列必然由“长度为 2 的序列 + 一个新元素”得到;
- 若只存长度 ≥ 3,会丢失扩展所需的中间状态;
- 统一存“长度 ≥ 2”,转移时自然区分是否计入答案。
4. 状态转移方程
4.1 枚举转移来源
固定右端点 \(i\),枚举左端点 \(j < i\): \[ d = a_i - a_j \] 若此前存在状态 \(f[j][d]\),表示:
- 有 \(f[j][d]\) 个等差子序列(长度 ≥ 2)
- 它们都可以接上 \(a_i\)
4.2 转移逻辑
答案累加
原来以 \(a_j\) 结尾、长度 ≥ 2 的序列, 接上 \(a_i\) 后长度 ≥ 3,全部是合法答案: \[ \text{ans} \;+=\; f[j][d] \]
状态更新
新的以 \(a_i\) 结尾、公差为 \(d\) 的序列包括:
- 从 \(f[j][d]\) 扩展而来的 \(f[j][d]\) 个;
- 新生成的长度为 2 的序列 \((a_j, a_i)\) 一个。
因此: \[ f[i][d] \;+=\; f[j][d] + 1 \]
4.3 完整状态转移方程
\[ \begin{aligned} \text{cnt} &= f[j][d] \quad (\text{若不存在则为 } 0) \\ \text{ans} &+= \text{cnt} \\ f[i][d] &+= \text{cnt} + 1 \end{aligned} \]
5. 边界与实现细节
5.1 差值溢出问题
数组元素是 int,但差值可能超过 int
范围,因此必须使用: \[
d = (long\ long)a_i - (long\ long)a_j
\] 否则会触发 有符号整数溢出(Undefined
Behavior)。
5.2 时间与空间复杂度
时间复杂度: \[ O(n^2) \] 枚举所有 \((j, i)\) 对。
空间复杂度: \[ O(n^2) \] 最坏情况下,每个位置可能存 \(O(n)\) 个不同差值。
6. AC代码
1 | class Solution { |