学习笔记《斜率优化》

简述

对于一个 \(dp_i = {\min/\max}_{j=1}^i dp_j + C_i + C_j + F_iF_j\)
由于转移中有同时和 \(i, j\) 相关的项,所以不能用单调队列优化。
所以需要斜率优化。

维护凸包

接下来以 \(\min\) 举例。
我们把式子改写为 \(dp_j+C_j=-F_iF_j+(dp_i-C_i)\)
\(y=dp_j+C_j, x=F_j, k=-F_i, b=dp_i-C_i\)
现在我们希望 \(b\) 越小越好,考虑怎么做。
我们把每个决策点变成平面上的点 \((x, y)\)
那么一条斜率为 \(k\) 的直线从下往上第一个碰到的点就是最优决策点。
我们考虑 \(j\)\(j'\) 更优的情况。

\[\begin{aligned} &dp_j + C_j + F_iF_j < dp_{j'} + C_{j'} + F_iF_{j'}\\ &F_i(F_j - F_{j'}) < (dp_{j'} + C_{j'}) - (dp_j + C_j)\\ &F_i < \dfrac{(dp_{j'} + C_{j'}) - (dp_j + C_j)}{F_j - F_{j'}} \end{aligned} \]

显然可能最优决策点的点会形成凸壳,考虑如何维护。
在大部分时候,取 \(\min\) 用下凸壳,取 \(\max\) 用上凸壳。

如果每次加入的点在最右边,用单调队列维护。
那么由于在最右边,所以肯定在凸壳内,然后向前删除不优的点即可。
如果 \(k\) 单调不降,每次从左往右,不优的点直接扔掉就好了。
否则就只能二分了。

如果不在最右边,可以用平衡树维护凸壳。
或者用 CDQ 离线处理。
由于右边不会成为左边决策点,所以先求左边,然后排序左侧建凸壳,更新 \([mid+1,r]\) 的答案后,继续递归右边。

维护直线

我们把 \(dp_i=dp_j+C_i+C_j+F_jF_j\) 改写成 \(dp_i-C_i=F_jF_i+(dp_j+F_j)\)
然后把决策点 \(j\) 看成 \(y=F_jx+(dp_j+F_j)\) 的一次函数。
每次要知道 \(x=F_i\) 时的最小值就好了,可以用李超线段树直接维护。

P2365 任务安排
\(O(n^3)\) 的 DP 是显然的 \(dp_{i, k} = \min_{j=1}^{n-1} dp_{j,k-1}+(\sum_{w=j+1}^i t_w+sk)(\sum_{w=j+1}^i f_w)\),考虑优化。
如果多分一段,那么后面的所有数都要加 \(sf\),所以我们可以提前加在 \(dp_i\),并对 \(t, f\) 做前缀和。
\(dp_i = \min_{j=1}^{n-1} dp_j + (f_i-f_j)t_i + s(f_n-f_j)\)

代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 5010;
int n, s, a[N], b[N], f[N];
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    cin >> n >> s;
    FL(i, 1, n)cin >> a[i] >> b[i], a[i] += a[i - 1], b[i] += b[i - 1];
    FL(i, 1, n){
        f[i] = 1e16;
        FL(j, 0, i - 1)
            cmin(f[i], f[j] + s * (b[n] - b[j]) + a[i] * (b[i] - b[j]));
    }
    cout << f[n];
    return 0;
}

P10979 任务安排2
考虑 \(n \le 3\times 10^5\) 怎么做,我们可以斜率优化。
我们拆一下式子(为方便不写 \(\min\)):
\(dp_i = dp_j + f_it_i - (s+t_i)f_j + sf_n\)
\(x=dp_j,y=f_j,k=s+t_i\)

代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
int n, s, a[N], b[N], f[N], q[N];
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    cin >> n >> s;
    FL(i, 1, n)cin >> a[i] >> b[i], a[i] += a[i - 1], b[i] += b[i - 1];
    int l = 1, r = 1;
    FL(i, 1, n){
        int k = s + a[i];
        while(l < r && f[q[l + 1]] - f[q[l]] <= k * (b[q[l + 1]] - b[q[l]]))l++;//直接淘汰
        f[i] = f[q[l]] - k * b[q[l]] + a[i] * b[i] + s * b[n];
        while(l < r && (f[q[r]] - f[q[r - 1]]) * (b[i] - b[q[r]]) >= (b[q[r]] - b[q[r - 1]]) * (f[i] - f[q[r]]))--r;//维护下凸壳
		q[++r] = i;
    }
    cout << f[n];
    return 0;
}

P5785 任务安排3
此题的 \(t_i\) 可能是负数,斜率不再单调,就不能直接淘汰小于的部分了。
由于下凸壳的斜率递增,我们可以用单调栈,询问时二分,复杂度 \(O(n \log n)\)

代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
int n, s, a[N], b[N], f[N], q[N];
int find(int k, int l, int r){
    if(l == r)return q[l];
    while(l < r){
        int m = l + r + 1 >> 1;
        if(f[q[m]] - f[q[m - 1]] <= k * (b[q[m]] - b[q[m - 1]]))l = m;
        else r = m - 1;
    }
    return q[l];
}
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    cin >> n >> s;
    FL(i, 1, n)cin >> a[i] >> b[i], a[i] += a[i - 1], b[i] += b[i - 1];
    int l = 1, r = 1;
    FL(i, 1, n){
        int k = s + a[i], p = find(k, l, r);
        f[i] = f[p] - k * b[p] + a[i] * b[i] + s * b[n];
        while(l < r && (f[q[r]] - f[q[r - 1]]) * (b[i] - b[q[r]]) >= (b[q[r]] - b[q[r - 1]]) * (f[i] - f[q[r]]))--r;
		q[++r] = i;
    }
    cout << f[n];
    return 0;
}

Ex.2

P4655 Building Bridges
我们设 \(sw_i = \sum_{j=1}^i w_j\)
然后考虑 \(O(n^2)\)\(dp_i = \min_{j=0}^{i-1}dp_j+(h_i-h_j)^2+(sw_{i-1}-sw_j)\)
然后愉快的拆式子。

\[\begin{aligned} &dp_i = dp_j+h_i^2+h_j^2-2h_ih_j+sw_{i-1}-sw_j\\ &dp_i = (dp_j+sw_j+h_j^2)-2h_ih_j+(h_i^2+sw_{i-1}) \end{aligned} \]

\(x=h_j,y=dp_j-sw_j+h_j^2,k=-2h_i\)
但是我们没有对 \(h\) 做前缀和,\(h\) 并不是单调的,所以每次加点并不一定在最右边。

CDQ
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
int n, h[N], w[N], s[N], Y[N], X[N], dp[N], pos[N], q[N];
long double slope(int j, int k){
	if (X[k] == X[j])return Y[k] > Y[j] ? 1e20 : -1e20;
	return (long double)(Y[k] - Y[j]) / (X[k] - X[j]);
}
void solve(int l, int r){
    if (l == r)
		return l = pos[l], Y[l] = dp[l] - s[l] + h[l] * h[l], X[l] = h[l], void();
	int mid = (l + r) >> 1;
	stable_partition(pos + l, pos + r + 1, [&](int&x){return x <= mid;});
	solve(l, mid);
    int L = 1, R = 0;
    FL(i, l, mid){
        while(L < R && slope(q[R], pos[i]) <= slope(q[R - 1], q[R]))R--;
        q[++R] = pos[i];
    }
    FL(i, mid + 1, r){
        int k = pos[i];
        while(L < R && slope(q[L], q[L + 1]) <= 2 * h[k])L++;
        cmin(dp[k], dp[q[L]] + (h[k] - h[q[L]]) * (h[k] - h[q[L]]) + s[k - 1] - s[q[L]]);
    }
	solve(mid + 1, r);
	inplace_merge(pos+l, pos+mid+1, pos+r+1, [&](int x, int y){return h[x] < h[y];});
}
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    cin >> n;
    FL(i, 1, n)cin >> h[i], pos[i] = i;
    FL(i, 1, n)cin >> w[i], s[i] = s[i - 1] + w[i];
    sort(pos + 1, pos + 1 + n, [&](int&x, int&y){return h[x] < h[y];});
	memset(dp, 0x7f, sizeof dp), dp[1] = 0, solve(1, n), cout << dp[n];
    return 0;
}

李超线段树
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
int h[N], w[N], dp[N], s[N], sg[N << 1], n;
struct A{int a, b;int g(int x){return b + a * x;}}line[N];
void modify(int u, int l, int r, int t){
    if(!sg[u])return void(sg[u] = t);
    if(l == r)return (line[sg[u]].g(l) > line[t].g(l)) && (sg[u] = t), void();
    int mid = l + r >> 1;
    if(line[t].g(mid) < line[sg[u]].g(mid))swap(t, sg[u]);
    if(line[t].g(l) < line[sg[u]].g(l))modify(u << 1, l, mid, t);
    if(line[t].g(r) < line[sg[u]].g(r))modify(u << 1 | 1, mid + 1, r, t);
}
int query(int u, int l, int r, int v){
    if(l == r)return line[sg[u]].g(v);
    int mid = l + r >> 1, ans = line[sg[u]].g(v);
    if(v <= mid)return min(ans, query(u << 1, l, mid, v));
    return min(ans, query(u << 1 | 1, mid + 1, r, v));
}
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    cin >> n, line[0].b = 1e18;
    FL(i, 1, n)cin >> h[i];
    FL(i, 1, n)cin >> w[i], s[i] = s[i - 1] + w[i];
    line[1] = {-2 * h[1], h[1] * h[1] - w[1]}, modify(1, 0, 1e6, 1);
    FL(i, 2, n){
        dp[i] = h[i] * h[i] + s[i - 1] + query(1, 0, 1e6, h[i]);
        line[i] = {-2 * h[i], dp[i] + h[i] * h[i] - s[i]}, modify(1, 0, 1e6, i);
    }
    cout << dp[n];
    return 0;
}

Ex.3

P4072 征途

\[\begin{aligned} vm^2 &= \dfrac{\sum_{i = 1}^{m} \left(\frac{S}{m} - v_i\right)^2}{m}\times m^2 \\ &= \left(\sum_{i = 1}^{m} \frac{S^2}{m^2}+v_i^2-2v_i\frac{S}{m}\right)\times m\\ &= S^2 + \sum_{i=1}^{m} mv_i^2 - 2Sv_i\\ &= -S^2 + m\sum_{i=1}^{m} v_i^2 \end{aligned} \]

我们带入 dp,设 \(f_{i, k}\) 表示前 \(i\) 分成 \(k\) 段时的最小值。

\[\begin{aligned} f_{i, k} &= \min_{j = k-1}^{i - 1}{f_{j, k-1} + (s_i - s_j)^2}\\ \end{aligned} \]

此时答案是 \(mf_{n, m}-s_n^2\),由于 \(n \le 3000\)
我们先枚举 \(k\),则上面的式子可以斜率优化。
\(f_{i, k} = (f_{j, k-1}+s_j^2) + s_i^2 - 2s_is_j\)
\(x=s_j,y=f_{j, k-1} + s_j^2,k=2s_i\)

代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
int s[N], f[N], g[N], q[N];
int X(int x){return s[x];}
int Y(int x){return s[x] * s[x] + g[x];}
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    int n, m;
    cin >> n >> m;
    FL(i, 1, n)cin >> s[i], s[i] += s[i - 1];
    FL(i, 1, n) g[i] = s[i] * s[i];
    FL(k, 2, m){
        int l = 1, r = 1;
        q[l] = k - 1;
        FL(i, k, n){
            while(l < r && Y(q[l + 1]) - Y(q[l]) < (X(q[l + 1]) - X(q[l])) * 2 * s[i])++l;
          	f[i] = g[q[l]] + (s[i] - s[q[l]]) * (s[i] - s[q[l]]);
            while(l < r && (Y(q[r]) - Y(q[r - 1])) * (X(i) - X(q[r])) > (Y(i) - Y(q[r])) * (X(q[r]) - X(q[r - 1])))r--;
            q[++r] = i;
        }
        FL(i, 1, n)g[i] = f[i];
    }
    // cout << s[n] << " " << f[n] << endl;
    cout << f[n] * m - s[n] * s[n];
    return 0;
}

Ex.4

CF1715E Long Way Home
\(k \le 20\),我们枚举 \(k\) 轮。
用上轮的答案计算再用 \(1\) 次航班后,每个点的最小值。
然后每次做最短路即可。

中间计算用斜率优化 DP 即可,\(f\) 是这轮的最小值,\(g\) 是上轮的。

\[\begin{aligned} f_i &= g_j + (i - j) ^ 2\\ &= (g_j+j^2) - 2ij + i^2 \end{aligned} \]

这里 \(g_j+j^2\) 为纵坐标,\(j\) 为横坐标,\(-2i\) 为斜率。

代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define FL(a,b,c) for(int a=(b),a##end=(c);a<=a##end;++a)
#define FR(a,b,c) for(int a=(b),a##end=(c);a>=a##end;--a)
#define lowbit(x) ((x)&-(x))
#define eb emplace_back
#define SZ(x) (int)((x).size())
#define int long long
#define vt vector
#define fr first
#define se second
#define ar(x) array<int,x>
#define PII pair<int, int>
#define max(a, b)({auto f7r=(a);auto j3h=(b);f7r<j3h?j3h:f7r;})
#define cmax(a, b)({auto j3h=(b);(j3h>a)&&(a=j3h);})
#define min(a, b)({auto f7r=(a);auto j3h=(b);f7r>j3h?j3h:f7r;})
#define cmin(a, b)({auto j3h=(b);(j3h<a)&&(a=j3h);})
constexpr int N = 1e6 + 10;
struct A{int x, dis;bool operator<(const A&j)const{return dis > j.dis;}};
priority_queue<A>p;
int f[N], n, m, k, q[N], g[N];
vt<PII>e[N];
void dij(){
    while(!p.empty()){
        int x = p.top().x, w = p.top().dis;
        if(p.pop(), w != f[x])continue;
        for(PII v : e[x])if(f[v.fr] > (w = f[x] + v.se))
            f[v.fr] = w, p.push({v.fr, w});
    }
}
int X(int x){return x;}
int Y(int x){return g[x] + x * x;}
double slope(int x, int y){
    return (double)(Y(y) - Y(x)) / (X(y) - X(x));
}
int32_t main(){
    cin.tie(0)->sync_with_stdio(0);
    int u, v, w, l, r;
    cin >> n >> m >> k;
    FL(i, 1, m)cin >> u >> v >> w, e[u].eb(v, w), e[v].eb(u, w);
    memset(f, 0x3f, sizeof f), f[1] = 0, p.push({1, 0}), dij();
    while(k--){
        q[l = r = 1] = 0;
        FL(i, 1, n)g[i] = f[i];
        FL(i, 1, n){
            while(l < r && slope(q[r - 1], q[r]) > slope(q[r], i))r--;
            q[++r] = i;
        }
        FL(i, 1, n){
            while(l < r && slope(q[l], q[l + 1]) < 2 * i)l++;
            int k = q[l];
            if(f[i] > (k = g[k] + (i - k) * (i - k)))
                f[i] = k, p.push({i, k});
        }
        dij();
    }
    FL(i, 1, n)cout << f[i] << " ";
    return 0;
}
posted @ 2025-07-27 10:03  fush's_blog  阅读(14)  评论(0)    收藏  举报