文章目录

斜率优化 DP

斜率优化 DP

0. 定位

斜率优化解决的是转移代价里含有 ij 的交叉乘积项」 的 DP:

把每个候选决策 j 看成一条直线,则 dp[i] 等于「在横坐标 x = a(i) 处,所有直线取值的最值」。
维护这些直线的凸包包络,每个 iO(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. 适用条件

必须同时满足:

  1. 能化成直线形式:转移可整理为 m(j)·x(i) + b(j) + C(i)(交叉乘项成功分离到直线)。
  2. 求区间最值min(下凸包)或 max(上凸包)。
  3. 按单调性分两类处理
    • 斜率 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 润色

posted @ 2026-09-13 16:13  lvwangshu  阅读(8)  评论(0)    收藏  举报