概述
斜率优化(Convex Hull Trick)将 DP 转移化为直线截距最值问题:。其中 是决策点, 是查询斜率。适用条件为转移方程可整理为 。
线性规划形式
以玩具装箱(P3195)为例:,整理得:
设 ,则 。
凸包维护
下凸壳用于 问题,上凸壳用于 问题。当 和 均单调时,用单调队列维护凸包:
deque<int> q;
q.push_back(0);
for (int i = 1; i <= n; ++i) {
while (q.size() >= 2 && slope(q[0], q[1]) <= k[i]) q.pop_front();
int j = q.front();
f[i] = y[j] - k[i] * x[j];
while (q.size() >= 2 && slope(q.back() - 1, q.back()) >= slope(q.back(), i))
q.pop_back();
q.push_back(i);
}二分斜率
当 不单调时,在凸包上二分查找斜率最接近 的边。当 也不单调时,需用 CDQ 分治或 平衡树 维护凸包。
问题模板
| 题目 | 题号 | 说明 |
|---|---|---|
| 玩具装箱 | P3195 | 经典入门,单调 单调 |
| 特别行动队 | P3628 | 形式 |
| 土地购买 | P2900 | 按 排序后斜率优化 |
| 征途 | SDOI2016 | 斜率优化 + 方差转换 |
| 仓库建设 | ZJOI2007 | 需注意数据类型范围 |
完整推导:从 DP 到
以 P3195 玩具装箱为例,详细演示转化过程。
原方程:
展开配方:
变量代换:
几何意义:对每个 ,有一条斜率为 的直线从 上移,第一个碰到的决策点 即为最优 。所有可能成为最优的决策点构成一个下凸壳。
凸包维护详解
单调队列维护( 单调, 单调)
当 随 递增,且 随 单调时,凸包可用单调队列维护:
using ll = long long;
struct Point { ll x, y; };
Point p[N];
deque<int> q;
double slope(int a, int b) {
return (double)(p[b].y - p[a].y) / (p[b].x - p[a].x);
}
void add_point(int i) {
// 维护下凸壳:新点必须使相邻斜率递增
while (q.size() >= 2 &&
slope(q[q.size()-2], q.back()) >= slope(q.back(), i))
q.pop_back();
q.push_back(i);
}
ll query(ll k) {
// 斜率单调递增时,队首即为最优
while (q.size() >= 2 && slope(q[0], q[1]) <= k) q.pop_front();
int j = q.front();
return p[j].y - k * p[j].x;
}判优条件(最小值,下凸壳): 时 最优。当 递增时,队首一旦被弹出就不会再用。
二分查找维护( 单调, 任意)
若 不单调(但 仍单调),仍可用单调队列维护凸包,但查询时改为二分:
ll query_binary(ll k) {
int l = 0, r = q.size() - 1;
while (l < r) {
int m = (l + r) / 2;
if (slope(q[m], q[m+1]) <= k) l = m + 1;
else r = m;
}
int j = q[l];
return p[j].y - k * p[j].x;
}在凸包上二分找到第一个斜率 的位置,该位置前一点即为最优。
Li Chao 线段树( 任意, 任意)
当 也不单调时,可使用 Li Chao 线段树在值域上维护直线集合,支持 插入与查询,无需关心凸包性质。适用于强制在线或纵坐标无序的场景。
struct Line {
ll k, b;
ll operator()(ll x) { return k * x + b; }
};
Line tree[N * 4];
void insert(int p, int l, int r, Line cur) {
int m = (l + r) / 2;
bool left = cur(l) < tree[p](l);
bool mid = cur(m) < tree[p](m);
if (mid) swap(tree[p], cur);
if (l == r) return;
if (left != mid) insert(p*2, l, m, cur);
else insert(p*2+1, m+1, r, cur);
}
ll query(int p, int l, int r, ll x) {
ll res = tree[p](x);
if (l == r) return res;
int m = (l + r) / 2;
if (x <= m) res = min(res, query(p*2, l, m, x));
else res = min(res, query(p*2+1, m+1, r, x));
return res;
}P3195 玩具装箱 完整题解
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 50010;
ll n, L, s[N], f[N];
deque<int> q;
ll X(int j) { return s[j]; }
ll Y(int j) { return f[j] + s[j] * s[j]; }
double slope(int a, int b) {
return (double)(Y(b) - Y(a)) / (X(b) - X(a));
}
int main() {
cin >> n >> L;
for (int i = 1; i <= n; ++i) {
cin >> s[i];
s[i] += s[i-1] + 1;
}
L++;
q.push_back(0);
for (int i = 1; i <= n; ++i) {
ll k = 2 * (s[i] - L);
while (q.size() >= 2 && slope(q[0], q[1]) <= k) q.pop_front();
int j = q.front();
f[i] = Y(j) - k * X(j) + (s[i] - L) * (s[i] - L);
while (q.size() >= 2 && slope(q[q.size()-2], q.back()) >= slope(q.back(), i))
q.pop_back();
q.push_back(i);
}
cout << f[n] << endl;
return 0;
}凸包性质速查
| 情形 | 维护方式 | 查询方式 | ||
|---|---|---|---|---|
| 全单调 | 递增 | 递增 | 单调队列 (deque) | 弹出队首 |
| 不单调 | 递增 | 任意 | 单调队列 (deque) | 二分凸包 |
| 不单调 | 任意 | 任意 | Li Chao 树 / CDQ | 线段树查询 |
| 最大值 | — | — | 上凸壳 (条件取反) | 对称处理 |
相关链接
多平台练习
| 洛谷 | 本题单 | 竞赛基础 |
| POJ (北大) | PKU JudgeOnline | 经典题目,适合巩固 |
| HDU (杭电) | HDU OJ | 暑期多校训练 |
| Codeforces | Codeforces | 国际竞赛,适合提升 |