斜率优化 DP 核心推导与笔记
斜率优化 DP 核心推导与笔记
一、适用形式
DP 转移方程可写成:
或最大值形式:
核心特征:\(f(j)\) 只与 \(j\) 有关,\(g(i)\) 只与 \(i\) 有关,两者以乘积形式出现。
二、点视角的转化
2.1 设点坐标
令:
原式中需要最小化的部分为:
记 \(E = g(i) \cdot x_j + y_j\),我们要求 \(\min E\)。
2.2 转化为几何问题
考虑一条斜率为 \(-g(i)\) 的直线:
当它过点 \((x_j, y_j)\) 时,截距为:
这正是我们要最小化的值。
所以,我们本质上就是要最小化 \(E\) 的值。
几何操作:用一条斜率为 \(-g(i)\) 的直线,从 y 轴负无穷远处(\(E\) 极小的方向)向上平移,它碰到的第一个点,就是使截距 \(E\) 最小的点。

如图,我们感性理解发现我们维护一个下凸壳(蓝色部分),所有不在下凸壳上的点一定会被包住,当直线从下放向上平移时,一定会优先碰到下凸壳上的点,所以所有不在下凸壳上的点就全部可以丢掉了。
因此总结:所有可能的最优解,都在点集的下凸壳上。
所以我们可以发现
求 \(\min\) → 斜率为 \(-g(i)\) 的直线从下往上碰 → 维护下凸壳
求 \(\max\) → 斜率为 \(-g(i)\) 的直线从上往下碰 → 维护上凸壳
然后我们可以发现几个性质:
- 加点的过程只依赖已算出的 \((x_j, y_j)\),只看点之间的几何位置关系,完全不关心 \(g(i)\) 是什么。
- 凸壳一旦维护好,对于后续任意查询斜率 \(-g(i)\),最优解一定在凸壳上,且可以高效定位。
- 凸壳就像一个建好的索引,查询什么值都保证返回正确结果。
因此可以用二分查找在凸壳上定位最优点。
若查询斜率也单调,可用单调队列在 \(O(1)\) 时间内维护队首。
三种情况的复杂度
| 条件 | 方法 | 复杂度 |
|---|---|---|
| \(f(j)\) 单调,\(g(i)\) 单调 | 单调队列 | \(O(n)\) |
| \(f(j)\) 单调,\(g(i)\) 不单调 | 单调栈 + 二分 | \(O(n \log n)\) |
| \(f(j)\) 不单调,\(g(i)\) 不单调 | 李超线段树 / CDQ 分治 | \(O(n \log n)\) |
\(f(j)\) 单调,\(K(i)\) 单调(g(i) 等价于 K(i))
决策点横坐标 \(X(j)\) 单调递增
\(X(j)\) 单调不减,意味着每一个加进来的点都在整个图的最右侧。这意味着我们在构建凸壳时,新的决策点总是从右边加入。
查询斜率 \(K(i)\) 单调递增
我们可以考虑每一条查询的线的斜率都比上一个大,那最优的点肯定在越来越右边。这意味着我们在查询最优决策点时,最优决策点的位置只会向右移动,不会回头。
当我们加入一个点时,我们的判断条件是:
也就是说,如果边 \(A \to B\) 的斜率大于等于边 \(B \to C\) 的斜率,那么 \(B\) 点就是凹进去的(或者三点共线),需要弹出。
我们转一下式子,判断条件转成下面这样:
就可以了。
而最优决策点的判断方式:
左边是队列前两个点连线的斜率,右边是当前查询的斜率 \(k\)。
所以这个循环翻译成人话就是:
如果队首第一个点和第二个点之间的斜率 \(\le\) 当前查询斜率,就把队首弹掉。
原理如下:
如果边 AB 的斜率 ≤k,说明直线从下方平移时,会先碰到 B,而不是 A。
因为 A→B 这段“坡度”不够陡,直线还没碰到 A 就已经越过它了。
如果边 BC 的斜率 >k,说明直线还没碰到 C,因为 B→C 这段太陡了。
所以最优解是 B。
结论:只要队首那条边的斜率 ≤k,队首点 A 就永远不会是最优解,可以安全扔掉。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n, dp[100005], q[100005], head, tail;
ll f[100005], sum[100005]; // f[j]单调, g(i)单调
// 求最小值版本
// 转移: dp[i] = min{ dp[j] + (sum[i]-sum[j])^2 + M } (典型例子)
// 化为: dp[i] = min{ (dp[j]+sum[j]^2) - 2*sum[i]*sum[j] } + sum[i]^2 + M
// 斜率比较: (y2-y1)/(x2-x1) <= k 时弹出队首
// 维护下凸壳
ll X(ll j) { return sum[j]; } // f(j)
ll Y(ll j) { return dp[j] + sum[j] * sum[j]; } // dp[j] + c(j)
ll K(ll i) { return 2 * sum[i]; } // g(i)
// 叉积判断: (Yb-Ya)/(Xb-Xa) <= (Yc-Yb)/(Xc-Xb)
// 即 (Yb-Ya)*(Xc-Xb) >= (Yc-Yb)*(Xb-Xa) 时弹出中间点(下凸壳)
bool check(ll a, ll b, ll c) {
return (Y(b) - Y(a)) * (X(c) - X(b)) >= (Y(c) - Y(b)) * (X(b) - X(a));
}
void solve_min() {
head = 1, tail = 0;
q[++tail] = 0; // 初始决策点
for (int i = 1; i <= n; i++) {
ll k = K(i);
// 弹出队首: 队首不如第二个优
while (head < tail && (Y(q[head+1]) - Y(q[head])) <= k * (X(q[head+1]) - X(q[head]))) {
head++;
}
ll j = q[head];
dp[i] = Y(j) - k * X(j) + sum[i] * sum[i]; // + h(i)
// 插入新决策点,维护下凸壳
while (head < tail && check(q[tail-1], q[tail], i)) {
tail--;
}
q[++tail] = i;
}
}
// 求最大值版本
// 维护上凸壳,不等式方向取反
bool check_max(ll a, ll b, ll c) {
return (Y(b) - Y(a)) * (X(c) - X(b)) <= (Y(c) - Y(b)) * (X(b) - X(a));
}
void solve_max() {
head = 1, tail = 0;
q[++tail] = 0;
for (int i = 1; i <= n; i++) {
ll k = K(i);
// 上凸壳: 队首不如第二个优
while (head < tail && (Y(q[head+1]) - Y(q[head])) >= k * (X(q[head+1]) - X(q[head]))) {
head++;
}
ll j = q[head];
dp[i] = Y(j) - k * X(j) + sum[i] * sum[i];
while (head < tail && check_max(q[tail-1], q[tail], i)) {
tail--;
}
q[++tail] = i;
}
}
本文来自博客园,作者:MistyPost,转载请注明原文链接。

浙公网安备 33010602011771号