斜率优化 DP
斜率优化DP
在单调队列优化过程中,转移方程被拆成了两部分,第一部分仅与
但仍然有很多转移方程,
其中
仔细观察上述式子,不难发现其实这是一个形如
而这种情况下,我们就需要维护上/下凸壳,且需要根据具体的题意,如
总结一下,在斜率优化问题中,每一个
trick:
斜率单调暴力移指针
斜率不单调二分找答案
建议可以先看最后的“一些常见问题”。
P3195 [HNOI2008] 玩具装箱
题目大意:
共有
考虑朴素 DP:
令
时间复杂度为
考虑优化:
将后面改写成一段只与
则(先省略
平方难以优化,展开得
有点斜率优化的影子了,移项得
将
接下来就是求解。
因此,类似线性规划,我们将这条直线从下往上平移,直到过第一个符合要求的点时停下,此时截距即为最小。
画出图像如下(红色为目标直线):
结合图像分析可知,本题中可能为最优的
显然,凸包中相邻两点斜率是单调递增的。
而目标直线的斜率
令
因为凸包和直线斜率均递增,我们可以用单调队列来维护这个凸包:
设队首为
-
对队首:
队首元素一直右移直到满足
。解释:如果
,显然 不是最优。可直接删去,因为目标直线斜率单调递增,所以当前删去的 一定对之后的 也不是最优,不会造成影响。 -
此时队首的点即为最优,根据它计算得出
。 -
对队尾:
队尾元素一直左移直到满足
。解释:如果
,说明 在凸包内部,一定没有 优,因此可以删去。 -
在队尾插入
。
最后注意初始化时要加入单调队列的点为
朴素前缀和优化 DP 代码:
Code
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 5e4+5;
ll c[N], sum[N];
ll dp[N]; // dp[i] 表示装完第 i 个玩具后所需费用的最小值
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, L; cin>>n>>L;
for(int i=1; i<=n; i++){
cin>>c[i];
sum[i] = sum[i-1]+c[i];
}
for(int i=1; i<=n; i++)
dp[i] = INT64_MAX;
for(int i=1; i<=n; i++){
for(int j=0; j<i; j++){
dp[i] = min(dp[i], dp[j]+(sum[i]+i-sum[j]-j-1-L)*(sum[i]+i-sum[j]-j-1-L)); // [j+1, i]
}
}
cout<<dp[n];
return 0;
}
正解加上斜率优化 DP 代码:
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
#define db double
const int N = 5e4+5;
int n, L;
ll c[N], sum[N];
ll Q[N], dp[N]; // dp[i] 表示装完第 i 个玩具后所需费用的最小值
db a(int x){return sum[x]+x;}
db b(int x){return a(x)+L+1;}
db X(int x){return b(x);}
db Y(int x){return (db)dp[x]+b(x)*b(x);}
db slope(int a, int b){return (Y(a)-Y(b))/(X(a)-X(b));}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>L;
for(int i=1; i<=n; i++){
cin>>c[i];
sum[i] = sum[i-1]+c[i];
}
int head = 1, tail = 0;
Q[++tail] = 0;
for(int i=1; i<=n; i++){
while(head<tail && slope(Q[head], Q[head+1])<=2*a(i)) head++;
dp[i] = dp[Q[head]]+(a(i)-b(Q[head]))*(a(i)-b(Q[head]));
while(head<tail && slope(i, Q[tail-1])<=slope(Q[tail], Q[tail-1])) tail--;
Q[++tail] = i;
}
cout<<dp[n];
return 0;
}
P5017 [NOIP2018 普及组] 摆渡车
题目大意:
有
注意:车回到 A 地后可以即刻出发。
我们不妨认为时间是一条数轴,每名同学按照到达时刻分别对应数轴上可能重合的点。安排车辆的工作,等同于将数轴分成若干个左开右闭段,每段的长度
令
以上算的是
考虑前缀和优化求和部分:
其中
至此,时间复杂度为
考虑剪去无用转移:
显然
考虑剪去无用状态:
假设正在求
可以证明有用的位置
因为我们要讲斜率优化 DP,所以我们要讲斜率优化 DP:
从前缀和优化后的式子开始,将括号拆开得
移项得
将
斜率
每个状态点最多进出队列一次,时间复杂度
还有一个小小的优化,考虑可从
朴素前缀和优化 DP 代码:
Code
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
constexpr int N = 10005;
int dp[N]; // dp[i] 表示 i 时刻前所有人的最小等待时间
int cnt[N], sum[N], t;
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
int ti; cin>>ti;
t = max(t, ti);
cnt[ti]++;
sum[ti] += ti;
}
for(int i=1; i<t+m; i++){
cnt[i] += cnt[i-1];
sum[i] += sum[i-1];
}
for(int i=0; i<t+m; i++){
dp[i] = cnt[i]*i-sum[i];
for(int j=0; j<=i-m; j++){
dp[i] = min(dp[i], dp[j]+(cnt[i]-cnt[j])*i-(sum[i]-sum[j]));
}
}
int ans = 1e9;
for(int i=t; i<t+m; i++)
ans = min(ans, dp[i]);
cout<<ans;
return 0;
}
剪枝优化后代码:
Code
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
constexpr int N = 4e6+5;
int dp[N]; // dp[i] 表示 i 时刻前所有人的最小等待时间
int cnt[N], sum[N], t;
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
int ti; cin>>ti;
t = max(t, ti);
cnt[ti]++;
sum[ti] += ti;
}
for(int i=1; i<t+m; i++){
cnt[i] += cnt[i-1];
sum[i] += sum[i-1];
}
for(int i=0; i<t+m; i++){
if(i>=m && cnt[i]==cnt[i-m]){
dp[i] = dp[i-m];
continue;
}
dp[i] = cnt[i]*i-sum[i];
for(int j=max(0, i-m-m+1); j<=i-m; j++){
dp[i] = min(dp[i], dp[j]+(cnt[i]-cnt[j])*i-(sum[i]-sum[j]));
}
}
int ans = 1e9;
for(int i=t; i<t+m; i++)
ans = min(ans, dp[i]);
cout<<ans;
return 0;
}
斜率优化后代码:
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
#define db long double
constexpr int N = 4e6+5;
int dp[N]; // dp[i] 表示 i 时刻前所有人的最小等待时间
int cnt[N], sum[N], t;
int Q[N], head = 1, tail = 0;
db X(int x){return cnt[x];}
db Y(int x){return (db)dp[x]+(db)sum[x];}
db slope(int a, int b){return X(a)==X(b) ? (Y(b)>Y(a)?1e9:-1e9) : (Y(a)-Y(b))/(X(a)-X(b));}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
int ti; cin>>ti; ti++; // +1 使 t=0 可转移而来
t = max(t, ti);
cnt[ti]++;
sum[ti] += ti;
}
for(int i=2; i<t+m; i++){
cnt[i] += cnt[i-1];
sum[i] += sum[i-1];
}
Q[++tail] = 0;
for(int i=1; i<t+m; i++){
if(i>m){ // 先判断可不可以由 i-m 转移
while(head<tail && slope(Q[tail-1], Q[tail])>=slope(Q[tail-1], i-m)) tail--; // tip1:两个点才能操作 tip2:等于号去重防止分母出锅
Q[++tail] = i-m;
}
while(head<tail && slope(Q[head], Q[head+1])<=i) head++;
dp[i] = dp[Q[head]]+cnt[i]*i-cnt[Q[head]]*i-sum[i]+sum[Q[head]];
}
int ans = 1e9;
for(int i=t; i<t+m; i++)
ans = min(ans, dp[i]);
cout<<ans;
return 0;
}
P5785 [SDOI2012] 任务安排
题目大意:
从零时刻开始,这些任务被分批加工,第
每个任务的费用是它的完成时刻乘以一个费用系数
设
每次开机对当前任务
这批任务的完成费用就是当前任务
设
至此,时间复杂度为
接下来省略
移项得
将
因为
询问的斜率可能先是蓝色的直线,再是红色的直线。所以要用队列维护一个下凸包,然后二分查找。
朴素前缀和优化 DP 代码:
Code
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 3e5+5;
ll st[N], sc[N];
ll dp[N]; // dp[i] 表示完成第 i 个任务后的最小费用值
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n, s; cin>>n>>s;
for(int i=1; i<=n; i++){
cin>>st[i]>>sc[i];
st[i] += st[i-1];
sc[i] += sc[i-1];
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for(int i=1; i<=n; i++){
for(int j=0; j<i; j++){
dp[i] = min(dp[i], dp[j]+s*(sc[n]-sc[j])+st[i]*(sc[i]-sc[j]));
}
}
cout<<dp[n];
return 0;
}
斜率优化 DP 代码:
#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 3e5+5;
ll st[N], sc[N];
ll dp[N], s; // dp[i] 表示完成第 i 个任务后的最小费用值
ll Q[N], head = 1, tail = 0;
ll X(ll x){return sc[x];}
ll Y(ll x){return dp[x]-s*sc[x];}
ll Search(ll l, ll r, ll k){
ll res = 0;
while(l<=r){
ll mid = (l+r)>>1;
if(Y(Q[mid+1])-Y(Q[mid]) < k*(X(Q[mid+1])-X(Q[mid]))){
l = mid+1;
} else{
res = mid;
r = mid-1;
}
}
return Q[res];
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin>>n>>s;
for(int i=1; i<=n; i++){
cin>>st[i]>>sc[i];
st[i] += st[i-1];
sc[i] += sc[i-1];
}
Q[++tail] = 0;
for(int i=1; i<=n; i++){
int j = Search(head, tail, st[i]);
dp[i] = dp[j]+s*(sc[n]-sc[j])+st[i]*(sc[i]-sc[j]);
while(head<tail && (__int128)(Y(Q[tail])-Y(Q[tail-1]))*(X(i)-X(Q[tail-1])) >= (__int128)(Y(i)-Y(Q[tail-1]))*(X(Q[tail])-X(Q[tail-1]))) tail--;
// 这里写成 (Y(Q[tail])-Y(Q[tail-1]))*(X(i)-X(Q[tail])) >= (Y(i)-Y(Q[tail]))*(X(Q[tail])-X(Q[tail-1])) 却不会爆 ll,即使两者是等价的
Q[++tail] = i;
}
cout<<dp[n];
return 0;
}
P3648 [APIO2014] 序列分割
题目大意:
你正在玩一个关于长度为
-
选择一个有超过一个元素的块(初始时你只有一块,即整个序列)。
-
选择两个相邻元素把这个块从中间分开,得到两个非空的块。
每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。
首先,这题最重要的是发现一个性质:答案与切的顺序无关。
-
证明:考虑将一块序列
分为 三块。方法一:先分成
。答案为 。方法二:先分成
。答案为 。
接下来就可以考虑 dp 了,设
至此,时间复杂度为
考虑如何进行斜率优化:
问题在于多了一个参数
拆开得
移项得
将
时间复杂度为
朴素前缀和优化 DP 代码 & 滚动数组后代码:
Code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5+5;
ll dp[N][205]; // dp[i][j] 表示前 i 个数切 j 刀的最大总得分
ll sum[N];
int fa[N][205];
int main(){
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
cin>>sum[i];
sum[i] += sum[i-1];
}
for(int i=1; i<=n; i++){
for(int k=1; k<=min(m, i-1); k++){
for(int j=0; j<i; j++){
if(dp[i][k] <= dp[j][k-1]+sum[j]*(sum[i]-sum[j])){
dp[i][k] = dp[j][k-1]+sum[j]*(sum[i]-sum[j]);
fa[i][k] = j;
}
}
}
}
cout<<dp[n][m]<<"\n";
for(int i=fa[n][m]; m; i=fa[i][--m])
cout<<i<<" ";
return 0;
}
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5+5;
ll dp[N]; // dp[i] 表示前 i 个数切 j 刀的最大总得分
ll g[N]; // g[i] 表示前 i 个数切 j-1 刀的最大总得分
ll sum[N];
int fa[N][205];
int main(){
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
cin>>sum[i];
sum[i] += sum[i-1];
}
for(int k=1; k<=min(m, n-1); k++){
for(int i=1; i<=n; i++)
g[i] = dp[i];
for(int i=k; i<=n; i++){
for(int j=0; j<i; j++){
if(dp[i] <= g[j]+sum[j]*(sum[i]-sum[j])){ // 可能出现最优解为 0 的可能,所以是 <= ' in:2 1 0 123 out:0 1
dp[i] = g[j]+sum[j]*(sum[i]-sum[j]);
fa[i][k] = j;
}
}
}
}
cout<<dp[n]<<"\n";
for(int i=fa[n][m]; m; i=fa[i][--m])
cout<<i<<" ";
return 0;
}
斜率优化 DP 代码:
#include<bits/stdc++.h>
#define ll long long
#define db long double
using namespace std;
const int N = 1e5+5;
ll dp[N]; // dp[i] 表示前 i 个数切 j 刀的最大总得分
ll g[N]; // g[i] 表示前 i 个数切 j-1 刀的最大总得分
ll sum[N];
ll Q[N], head = 1, tail = 0;
int fa[N][205];
ll Y(int x){return g[x]-sum[x]*sum[x];}
ll X(int x){return sum[x];}
db slope(int a, int b){return X(a)==X(b) ? (Y(b)>=Y(a)?1e18:-1e18) : (Y(b)-Y(a))/(db)(X(b)-X(a));}
int main(){
int n, m; cin>>n>>m;
for(int i=1; i<=n; i++){
cin>>sum[i];
sum[i] += sum[i-1];
}
for(int k=1; k<=min(m, n-1); k++){
for(int i=1; i<=n; i++)
g[i] = dp[i];
head = 1, tail = 0;
Q[++tail] = 0;
for(int i=k; i<=n; i++){
while(head<tail && slope(Q[head], Q[head+1])>=(-sum[i])) head++;
dp[i] = g[Q[head]]+sum[Q[head]]*(sum[i]-sum[Q[head]]);
fa[i][k] = Q[head];
while(head<tail && slope(Q[tail-1], Q[tail])<=slope(Q[tail-1], i)) tail--;
Q[++tail] = i;
}
}
cout<<dp[n]<<"\n";
for(int i=fa[n][m]; m; i=fa[i][--m])
cout<<i<<" ";
return 0;
}
一些常见问题
return Y(j)>=Y(i) ? inf : -inf
,而不要直接返回 inf
或者 -inf
,在某些题中情况较复杂,如果不小心画错了图,返回了一个错误的极值就完了,而且这种错误只用简单数据还很难查出来。
head=1, tail=0
,由于塞了初始点导致 head=tail=1
甚至是 head=t=0, head=tail=2
之类的写法,其实是因为省去了塞初始点的代码。它们都是等价的。
head <= tail
,而出入队判断都需要有至少两个元素才能进行操作。所以应是 head < tail
。
slope(Q[tail-1],Q[tail])<=slope(Q[tail],i)
,另一种是 slope(Q[tail-1],Q[tail])<=slope(Q[tail-1],i)
, 都表示出现了可以删去点
参考资料
DP 转移方程 —— 单调队列优化 & 斜率优化 & 李超树优化
等等
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现