斜率优化 DP
斜率优化 DP
0. 定位
斜率优化解决的是转移代价里含有 「i 与 j 的交叉乘积项」 的 DP:
把每个候选决策
j看成一条直线,则dp[i]等于「在横坐标x = a(i)处,所有直线取值的最值」。
维护这些直线的凸包包络,每个i在O(1)(单调情形)或O(log n)(任意情形)内取到最优。
1. 动机:单调队列为什么不够
单调队列优化的前提是可加性分离:
dp[i] = g(i) + opt{ f(j) } # 只与 j 有关 + 只与 i 有关,无交叉
但很多转移长这样:
dp[i] = min_{j<i} ( dp[j] + A(i)·B(j) + C(i) + D(j) ) # 含 A(i)·B(j) 交叉项
A(i)·B(j) 既含 i 又含 j,拆不成 f(j)+g(i),单调队列失效。
斜率优化的核心动作,就是把这种交叉乘积项「卷」成直线,再用几何方法取最值。
2. 本质原理
2.1 直线化变换(最关键的一步)
把转移展开并重新分组,目标是凑出 m(j)·x(i) + b(j):
dp[i] = C(i) + min/max_{j} ( m(j) · x(i) + b(j) )
- 每个决策
j⟶ 一条直线y = m(j)·x + b(j)- 斜率
m(j)只与j有关 - 截距
b(j)只与j有关(通常含dp[j])
- 斜率
- 主状态
i只决定查询横坐标x = x(i) dp[i] = C(i) + 在 x(i) 处所有直线的最值
注:式子必须写成「
(只关于 j 的系数) × (只关于 i 的变量) + (只关于 j 的常数)」。
2.2 几何意义:凸包包络
- 求 min → 维护所有直线的下凸包(lower envelope);
- 求 max → 维护上凸包(upper envelope)。
- 在任意横坐标处,最优直线一定落在凸包包络上,内部被包住的直线永不最优。
2.3 为什么要这样:决策被几何淘汰
两条直线 L₁, L₂ 在某个横坐标 x* 相交;在 x* 一侧 L₁ 更优,另一侧 L₂ 更优。
若有三条直线 L₁→L₂→L₃,且 L₂ 与 L₁ 的交点已经 ≥ L₂ 与 L₃ 的交点,说明 L₂ 的「最优区间」被压没——它永远不可能最优,直接永久淘汰。
注:这就是斜率优化与单调队列的共性,用「更优且更晚被淘汰 → 永久淘汰」的支配关系,把候选集压成一条凸包。 区别只在于「比较的对象」:单调队列比的是单点值,斜率优化比的是整条直线。
3. 适用条件
必须同时满足:
- 能化成直线形式:转移可整理为
m(j)·x(i) + b(j) + C(i)(交叉乘项成功分离到直线)。 - 求区间最值:
min(下凸包)或max(上凸包)。 - 按单调性分两类处理:
- 斜率
m(j)与查询x(i)都单调 → 用单调队列/双端队列维护凸包,插入、查询均摊O(1),总O(n)。这是「斜率优化 + 单调队列」最常见的形态(进阶指南把它紧接在单调队列之后讲,原因在此)。 - 两者不单调 → 用**李超树(Li Chao Tree)**或
std::set动态维护,总O(n log n),无需任何单调性假设。
- 斜率
不可用反例:
- 转移是纯加性
f(j)+g(i)却硬套直线 → 杀鸡用牛刀,单调队列更简单。 opt(i)单调但代价不是直线(如只满足四边形不等式)→ 该用分治优化,不是斜率优化。- 交叉项非线性(如
A(i)·B(j)²)→ 直线化失败,通常需 CDQ 分治 + 李超树或换思路。
4. 通用形态与核心操作
4.1 交点公式
直线 L(m,b) 与 L'(m',b') 的交点横坐标:
x* = (b' - b) / (m - m')
实际代码用叉乘避免浮点与符号错误:
(m - m')·(b' - b) 的大小比较 ⟺ 比较交点左右
4.2 下凸包维护(单调情形,斜率递增、查询递增)
// 维护下凸包,直线按斜率 m 递增加入;查询横坐标 x 递增
struct Line { long long m, b; // y = m*x + b
long long operator()(long long x) const { return m*x + b; } };
// 交点横坐标(叉乘形式,避免除法)
bool bad(Line a, Line b, Line c) {
// 若 a 与 c 的交点 <= a 与 b 的交点,则 b 被夹死,弹出
return (b.b - a.b) * (a.m - c.m) >= (c.b - a.b) * (a.m - b.m);
}
deque<Line> dq;
// 插入新直线 L:
while (dq.size() >= 2 && bad(dq[dq.size()-2], dq.back(), L)) dq.pop_back();
dq.push_back(L);
// 查询在 x 处的最小值:
while (dq.size() >= 2 && dq[0](x) >= dq[1](x)) dq.pop_front();
ans = dq[0](x);
要点:
- 插入时从队尾剔除「被夹死」的直线(
bad条件);- 查询时从队首弹出「已被更优直线取代」的直线;
- 若实际算出的斜率是递减的,等价于维护上凸包或把斜率取负,逻辑镜像处理即可。
4.3 李超线段树(Li Chao Tree):任意情形下的通用结构
动机:为什么需要它
当斜率 m(j) 或查询 x(i) 不单调时,双端队列维护凸包会失效:
- 新直线的「最优区间」可能插在已有凸包的中间,无法只从队尾剔除;
- 查询也不能只弹队首(最优直线不一定在队首)。
于是需要一种支持任意顺序插入、任意横坐标查询的结构。李超树正是为这个问题设计的通用方案,且不要求任何单调性。
核心思想
用一棵线段树把横坐标定义域 [Xmin, Xmax] 切成若干区间。每个节点对应一个 x 区间,只存一条「在该区间中点处取值最优」的直线(称为该节点的「优势直线」)。
注:
为什么只存「中点最优」这一条就够?
关键不变量:对任意查询横坐标x,真正最优的那条直线,一定出现在「从根到x所在叶子的路径」上某个节点的优势直线中。 因此查询时只要沿路径取所有优势直线的最值即可,无需在每个节点存完整凸包。关键性质:若直线
L在某个区间中点比当前优势直线更优,那么L至少在该区间的「含中点的那一半」比它优——于是把L设为该节点优势直线,把被换下的旧直线递归下放去和另一半比较。每次插入只沿一条路径递归,深度O(log V)。
插入(insert)
// 求最小值版本。线段树节点 u 管区间 [l, r],tree[u] 存该区间中点最优直线
struct Line { long long m, b; // y = m*x + b
long long eval(long long x) const { return m*x + b; } };
Line tree[SIZE];
bool empty[SIZE] = {true};
void add(int u, int l, int r, Line k) {
if (empty[u]) { tree[u] = k; empty[u] = false; return; }
int mid = (l + r) >> 1;
bool win_mid = k.eval(mid) < tree[u].eval(mid); // k 在中点更优?
if (win_mid) swap(tree[u], k); // 中点更优者留节点,败者下放
if (l == r) return;
if (k.eval(l) < tree[u].eval(l)) // 败者(现 k)在左端点更优 → 交点在左半
add(u*2, l, mid, k);
else // 否则交点在右半(或整段被支配)
add(u*2+1, mid+1, r, k);
}
几何直觉:k 与节点原直线至多相交一次。在中点比较决定「谁留节点」;被换下的败者只在「它仍可能更优」的那一侧(由左端点比较判定)继续下放。这样每条直线沿一条路径走到底,绝不回退。
查询(query)
long long ask(int u, int l, int r, long long x) {
if (empty[u]) return INF; // 无直线时返回极大值(求 min 情形)
long long res = tree[u].eval(x);
if (l == r) return res;
int mid = (l + r) >> 1;
if (x <= mid) res = min(res, ask(u*2, l, mid, x));
else res = min(res, ask(u*2+1, mid+1, r, x));
return res;
}
查询沿根到叶子的一条路径,把路径上所有优势直线在该 x 处的值取最值 —— 由前述不变量,结果即为全局最优。
复杂度
- 每次插入 / 查询深度 = 树高 =
O(log V),V为横坐标定义域长度。 - 总复杂度
O(n log V)(n条直线)。无需任何单调性假设。
坐标范围与动态开点
- 若
x范围大(如1e9)但直线数少,用动态开点线段树(指针或map节点)避免建满整棵树; - 若
x是实数域或稀疏,可把查询点离散化后建树; - 区间范围需在插入前根据数据估计并统一(通常取
[-INF, INF]或坐标上下界)。
与双端队列凸包对比
| 双端队列凸包 | 李超线段树 | |
|---|---|---|
| 单调性要求 | 斜率 & 查询都必须单调 | 无要求 |
| 复杂度 | O(n) 均摊 |
O(n log V) |
| 常数项 | 极小 | 较大 |
| 删除直线 | 易(滑动窗口可踢) | 难(通常不可删,需持久化/整体二分/分治重构) |
| 适用 | 单调情形首选 | 任意/在线查询兜底 |
注:单调能上 deque 就别用李超树;不单调或需在线任意查询,再上李超树。
变体与注意
- 求最大值:把所有比较的
<改成>即可(上凸包即最大)。李超树天然不分上下,仅比较符号不同。 - 维护线段(直线只在
[xl, xr]内有效):插入时若区间完全在某侧则直接返回,否则按中点裁剪后递归,逻辑略繁但思想一致。 - 正确性核心:节点只存「中点最优」,但查询沿路径取最值必得全局最优 —— 这是李超树成立的根,务必理解而非死记模板。
5. 经典模型精析
模型 A | 玩具装箱(Toy Packing,P3195 / POJ 3709)
- 建模:把物品分 contiguous 段,段
[j+1,i]代价(sᵢ−sⱼ+i−j−1−L)²。令f[i]=s[i]+i, C=L+1, g[j]=f[j]+C:
dp[i] = f[i]² + minⱼ( (−2g[j])·f[i] + (dp[j]+g[j]²) )。 - 优化关键:每个
j是直线,斜率mⱼ=−2g[j](随j递减),查询x=f[i](递增)→ 下凸包 + 单调队列,O(n)。
模型 B | 土地征用(Land Acquisition,POJ 1180 / CF)
- 建模:矩形按宽排序、去「被完全包含」的冗余后,段代价化成直线。
- 优化关键:先预处理(排序+单调栈去冗余)把问题变成直线形式,再套斜率优化。体现「斜率优化前常需一层贪心/排序化简」。
模型 C | 任务安排 / 批量调度(Batch)
- 建模:连续任务分段,段代价含段长与段内任务和的乘项,
dp展开后出现交叉项。 - 优化关键:典型「含
i·j交叉项」的入门直线化练习,斜率与查询均单调 → 单调队列凸包。
模型 D | 带窗口限制(决策 j ∈ [i−k, i−1])
- 建模:转移同前,但
j只能取最近k个。 - 优化关键:在凸包单调队列基础上,插入时额外从队首踢出超出窗口的旧决策——即「斜率优化 + 滑动窗口」二合一。
模型 E | 任意查询(李超树)
- 代表:斜率 / 查询不单调的题(如部分 DP 值查询顺序不可控)。
- 优化关键:放弃单调性假设,直接上李超树(结构、插入/查询、动态开点见 4.3),
O(n log V),模板通用。
6. 本质再认识
- 斜率优化 vs 单调队列:交叉乘积项
m(j)·x(i)⇔ 直线化;加性项f(j)+g(i)⇔ 单点值。前者比的是整条直线(凸包),后者比的是单点值(滑窗)。 - 斜率优化 vs 分治优化:分治优化只要求
opt(i)单调、代价可快速合并,不要求直线;斜率优化要求代价是线性交叉形式。 - 单调队列是特例:当斜率与查询都单调时,凸包退化成可用双端队列
O(n)维护——这正是它紧挨着单调队列讲的原因。 - 边界意识:直线化的代数展开最容易出错(符号、常数项
C(i)别落进凸包里);先写清「m(j)、b(j)、x(i)、C(i)各是什么」再编码。
7. 总结
斜率优化 DP = 交叉乘积项 → 直线 → 凸包包络。
把每个决策j卷成直线y=m(j)x+b(j),把dp[i]变成在x=x(i)处取直线最值;用凸包淘汰永不最优的直线。
判定它能否用:看转移能不能化成m(j)·x(i)+b(j);单调就上双端队列O(n),不单调就上李超树O(n log n)。
部分内容由 AI 润色
原创·转载请注明出处
✦
浙公网安备 33010602011771号