斜率优化 DP 核心推导与笔记

斜率优化 DP 核心推导与笔记

一、适用形式

DP 转移方程可写成:

\[dp[i] = \min_{j < i} \{ f(j) \cdot g(i) + dp[j] \} + h(i) \]

或最大值形式:

\[dp[i] = \max_{j < i} \{ f(j) \cdot g(i) + dp[j] \} + sum \]

核心特征:\(f(j)\) 只与 \(j\) 有关,\(g(i)\) 只与 \(i\) 有关,两者以乘积形式出现。

二、点视角的转化

2.1 设点坐标

令:

\[x_j = f(j),y_j = dp[j] \]

原式中需要最小化的部分为:

\[f(j) \cdot g(i) + dp[j] = g(i) \cdot x_j + y_j \]

记 \(E = g(i) \cdot x_j + y_j\),我们要求 \(\min E\)。

2.2 转化为几何问题

考虑一条斜率为 \(-g(i)\) 的直线:

\[y = -g(i) \cdot x + E \]

当它过点 \((x_j, y_j)\) 时,截距为:

\[E = y_j + g(i) \cdot x_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)\) 单调递增

我们可以考虑每一条查询的线的斜率都比上一个大,那最优的点肯定在越来越右边。这意味着我们在查询最优决策点时,最优决策点的位置只会向右移动,不会回头。

当我们加入一个点时,我们的判断条件是:

\[\frac{Y(b) - Y(a)}{X(b) - X(a)} \ge \frac{Y(c) - Y(b)}{X(c) - X(b)} \]

也就是说,如果边 \(A \to B\) 的斜率大于等于边 \(B \to C\) 的斜率,那么 \(B\) 点就是凹进去的(或者三点共线),需要弹出。

我们转一下式子,判断条件转成下面这样:

\[[Y(b) - Y(a)]\times [X(c) - X(b)] \ge [X(b) - X(a)] \times [Y(c) - Y(b)] \]

就可以了。

而最优决策点的判断方式:

\[\frac{Y(q[\text{head}+1]) - Y(q[\text{head}])}{X(q[\text{head}+1]) - X(q[\text{head}])} \le k \]

左边是队列前两个点连线的斜率,右边是当前查询的斜率 \(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;
    }
}
posted @ 2026-07-21 20:12  MistyPost  阅读(23)  评论(2)    收藏  举报