斜率优化
斜率优化是一种对于形如 \(F_{i}=\min/\max\{F_j+...\}\) 的转移方程式,将其结构变换从而与几何联系起来,转化为坐标系内求凸包切线的问题。
结合例题讲解。
【例1】任务安排 1
说明:题目中的 \(f\) 被我篡改成了 \(c\)。
列出朴素转移方程。设 \(F(i,j)\) 表示前 \(i\) 物品分成 \(j\) 段的最小费用,\(C[i],T[i]\) 分别为 \(c_i,t_i\) 前缀和。
时间复杂度 \(O(n^3)\)。
一个常用的技巧叫做 费用提前计算:考虑到
- 题目并未要求具体分几段,因此 \(j\) 维理论可以省掉
- \(k+1\sim i\) 多分成一段,造成的影响 是后面所有的物品完成时间 \(+S\)
因此,可以写出
相当于是把费用给在产生时就计算了。由于 ________________________(坑待填)
,这种优化方式是不会造成错误决策的。
时间复杂度 \(O(n^2)\),可以通过。
#include <bits/stdc++.h>
using namespace std;
const int N=5005;
int n,S,t[N],c[N];
long long f[N];
int main(){
cin>>n>>S;
for(int i=1;i<=n;i++)cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
f[i]=min(f[i],f[j]+1ll*(c[i]-c[j])*t[i]+S*(c[n]-c[j]));
cout<<f[n];
}
【例2】任务安排 2
在【例1】基础上,\(n\le 3*10^5\)。
先把 \(\min\) 去掉。
再展开。
再将只关于 \(j\) 的、同时关于 \(i,j\) 的、常数项区分开。由于所在是 \(i\),因此含 \(i\) 的(例如 \(T[i],C[i]\))就等同不变量(常数)。
只关于 \(j\) 的:放在等号左边。
这样,可以看成一个一次函数 \(y=kx+b\)。其中 \(y=F[j],k=S+T[i],x=C[j],b=F[i]-C[i]T[i]-SC[n]\)。
只需要最小化截距,就相当于最小化 \(F[i]\)。
考虑这个式子的含义,发现斜率是定值,而必须经过坐标系的若干点 \((C[j],F[j])\)。也就是一条斜率一定的直线在坐标系内自下而上移动,碰到的第一个点就是使截距最小化的点。这一步叫做线性规划。
那么求这些点的凸包,一个容易发现的结论是该直线一定经过下凸壳上的点。
考虑决策单调性。发现 \((C[j],F[j])\) 随着 \(j\) 的增大,\(C[j]\) 增大,所以可以一般地维护下凸壳,利用一个单调数据结构。我们可以用单调队列。
在本题中,每确定一个 \(F[i]\),就将 \((C[i],F[i])\) 从队尾加入下凸壳并维护下凸壳。同时,观察到 \(S+T[i]\) 是递增的,因此切点左侧的凸壳点都可以丢掉,所以可以逐一弹掉满足 \(q[l],q[l+1]\) 的连线斜率 \(\le S+T[i]\) 的队头 \(l\)。 总复杂度 \(O(n)\)。
【例3】任务安排 3
与【例2】的唯一区别为:\(t_i\) 可以为负数。
这说明斜率 \(S+T[i]\) 不再单调递增。所以必须保留所有凸壳点。
而只需要在单调队列(此时也可以替换为单调栈)里二分一个最靠前的点 \(q[mid]\),使得 \(q[mid],q[mid+1]\) 所连接的线段的斜率大于 \(S+T[i]\),就能找到切点 \(q[mid]\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,S,tp,l=1,r=0,t[N],c[N],q[N],f[N];
signed main(){
cin>>n>>S;
for(int i=1;i<=n;i++)cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
q[++r]=0;
for(int i=1;i<=n;i++){
int L=l-1,R=r,mid;
while(L<R-1){
mid=L+R>>1;
if((f[q[mid+1]]-f[q[mid]])>(S+t[i])*(c[q[mid+1]]-c[q[mid]]))R=mid;
else L=mid;
}
f[i]=f[q[R]]+(c[i]-c[q[R]])*t[i]+S*(c[n]-c[q[R]]);
while(r>l&&(c[q[r]]-c[q[r-1]])*(f[i]-f[q[r-1]])-(c[i]-c[q[r-1]])*(f[q[r]]-f[q[r-1]])<=0)r--;
q[++r]=i;
}
cout<<f[n];
}
一个非常容易错的点是凸壳操作的取等问题,参见 here。
【例4】玩具装箱
斜率优化模板题。属于【例2】类型,\(O(n)\)。在上一题中,我们将斜率的分母乘到了另一边,十字相乘了,但这样有两个条件:
-
分母是正数
-
不会爆 LL
本题不满足第二个条件,所以采用相对不保险,其实算保险的浮点数斜率。
#include <bits/stdc++.h>
#define int long long
#define y(i) (f[i]+s[i]*s[i])
using namespace std;
const int N=5e4+5;
int n,L,l=1,r,s[N],q[N],f[N];
signed main(){
cin>>n>>L,L++;
for(int i=1;i<=n;i++)cin>>s[i],s[i]+=s[i-1]+1;
q[++r]=0;
for(int i=1;i<=n;i++){
while(l<r&&1.0*(y(q[l+1])-y(q[l]))/(s[q[l+1]]-s[q[l]])<2*(s[i]-L))l++;
f[i]=f[q[l]]+(s[i]-L)*(s[i]-L)+s[q[l]]*s[q[l]]-2*s[q[l]]*(s[i]-L);
while(l<r&&1.0*(s[q[r]]-s[q[r-1]])/(y(q[r])-y(q[r-1]))<=1.0*(s[i]-s[q[r-1]])/(y(i)-y(q[r-1])))r--;
q[++r]=i;
}
cout<<f[n];
}
【例5】CF311B Cats Transport
容易想到将每只猫以 \((t_i,y_i)\) 的形式放到坐标系里,而 feeder 则是一条斜率 =1 的直线,其中 \(y_i=D_{h_i}\),\(D_{h_i}\) 表示 0 到 \(h_i\) 的距离,则题意转为
用至多 \(p\) 条直线来划分这 \(n\) 个点,使得代价最小。代价的计算方式为过该点作一条平行横轴直线,直线与右侧 feeder 直线的第一个交点到该点的距离。
首先显然最优情况下一定 feeder 直线是要过一个点的。考察这个点,假如记作 \((X,Y)\),那么 \((x,y)\) 分给他的代价为 \((X-x+y-Y)=(X-Y)-(x-y)\)。
因此先将点排好序:
bool operator<(cat a,cat b){
return a.t-a.y<b.t-b.y;
}
这样就可以 dp 了。
展开得到
整理得
就可以斜率优化了。这时再看重载运算符,发现刚好保证了斜率单调性,是不是很巧妙呢?
这题一定不能把分母乘到另一边因为两者都不满足。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m,p,h[N],d[N],T[N],Y[N],f[2][N],q[N];
struct cat{
int t,y;
}a[N];
bool operator<(cat a,cat b){
return a.y-b.y+b.t-a.t>0;
}
signed main(){
cin>>m>>n>>p;
for(int i=2;i<=m;i++)cin>>d[i],d[i]+=d[i-1];
for(int i=1;i<=n;i++)cin>>h[i]>>a[i].t,a[i].y=d[h[i]];
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)T[i]=T[i-1]+a[i].t,Y[i]=Y[i-1]+a[i].y;
memset(f,0x3f,sizeof(f));
int ans=1e18;
f[0][0]=0;
for(int w=1;w<=p;w++){
f[w&1][0]=0;
int l=1,r=0;q[++r]=0;
for(int i=1;i<=n;i++){
while(l<r&&1.0*((f[w&1^1][q[l+1]]+T[q[l+1]]-Y[q[l+1]])-(f[w&1^1][q[l]]+T[q[l]]-Y[q[l]]))/(q[l+1]-q[l])<a[i].t-a[i].y)l++;
f[w&1][i]=f[w&1^1][q[l]]+(i-q[l])*(a[i].t-a[i].y)-T[i]+T[q[l]]+Y[i]-Y[q[l]];
while(l<r&&1.0*((f[w&1^1][q[r]]+T[q[r]]-Y[q[r]])-(f[w&1^1][q[r-1]]+T[q[r-1]]-Y[q[r-1]]))/(q[r]-q[r-1])>=1.0*((f[w&1^1][i]+T[i]-Y[i])-(f[w&1^1][q[r-1]]+T[q[r-1]]-Y[q[r-1]]))/(i-q[r-1]))r--;
q[++r]=i;
}
ans=min(ans,f[w&1][n]);
}
cout<<ans;
}
【例6】(分治+斜率优化)ARC066D Contest with Drinks Hard
容易想到求出前后缀的最大得分。设 \(f[i]\) 和 \(g[i]\) 表示前/后缀 \(i\) 中最大成绩:
f[i]<-max(1<=j<=i-1){f[j]+(i-j)(i-j+1)/2-s[i]+s[j]}$$
1. 斜率优化: f[j]+(j-1)*j/2+s[j]=i*j+f[i]+s[i]-(i+1)*i/2
2. f[i]=max(f[i],f[i-1])
g[i]<-max(i+1<=j<=n){g[j]+(j-i)(j-i+1)/2-s[j-1]+s[i-1]}
1. 斜率优化: g[j]+j*(j+1)/2-s[j-1]=i*j+g[i]-s[i-1]+i*(i-1)/2
2. g[i]=max(g[i],g[i+1])
很难直接快速求每个修改的位置的答案,可以考虑分治来整体地求。
对于分治区间[L,R],分成[L,mid]和[mid+1,R]
设h[i]-t[i]+x[i]表示i的答案
设p[i],q[i]表示i为左端点的答案和为右端点的答案
p[i+1]<-max(mid+1<=j<=R){f[i]+g[j+1]-(s[j]-s[i])+(j-i)(j-i+1)/2}
g[j+1]-s[j]+(j+1)*j/2=i*j+(p[i+1]-s[i]-f[i]+(1-i)*i/2)
q[i]<-max(L-1<=j<=mid-1){f[j]+g[i+1]-(s[i]-s[j])+(i-j)(i-j+1)/2}
f[j]+(j-1)*j/2+s[j]=i*j+(q[i]+s[i]-g[i+1]-(i+1)*i/2)
p[i]->h[L~mid],q[i]->h[mid+1->R]
复杂度:\(T(n)=2T(n/2)+O(n)=O(n\log n)\)
#include <bits/stdc++.h>
#define int long long
#define yf(j) (f[j]+(j-1)*j/2+s[j])
#define yg(j) (g[j]+j*(j+1)/2-s[j-1])
#define yp(j) (g[j+1]-s[j]+(j+1)*j/2)
#define yq(j) (f[j]+(j-1)*j/2+s[j])
using namespace std;
const int N=3e5+5;
int n,m,T[N],P[N],X[N],f[N],g[N],que[N],p[N],q[N],h[N],s[N];
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
void solve(int L,int R){
if(L==R)return;
int mid=L+R>>1;
solve(L,mid),solve(mid+1,R);
int r=0;//stressed,0insteadof1
for(int i=mid+1;i<=R;i++){
while(1<r&&1.0*(yp(que[r])-yp(que[r-1]))/(que[r]-que[r-1])<1.0*(yp(i)-yp(que[r]))/(i-que[r]))r--;
que[++r]=i;
}
int mx=-1e18;//stressed
for(int i=L-1;i<mid;i++){
while(1<r&&1.0*(yp(que[r])-yp(que[r-1]))/(que[r]-que[r-1])<i)r--;
p[i+1]=f[i]+g[que[r]+1]-(s[que[r]]-s[i])+(que[r]-i)*(que[r]-i+1)/2;
mx=max(mx,p[i+1]);
h[i+1]=max(h[i+1],mx);
}
r=0;
for(int i=L-1;i<mid;i++){
while(1<r&&1.0*(yq(que[r])-yq(que[r-1]))/(que[r]-que[r-1])<1.0*(yq(i)-yq(que[r]))/(i-que[r]))r--;
que[++r]=i;
}
mx=-1e18;//stressed
for(int i=mid+1;i<=R;i++){
while(1<r&&1.0*(yq(que[r])-yq(que[r-1]))/(que[r]-que[r-1])<i)r--;
q[i]=f[que[r]]+g[i+1]-(s[i]-s[que[r]])+(i-que[r])*(i-que[r]+1)/2;
}
for(int i=R;i>mid;i--)mx=max(mx,q[i]),h[i]=max(h[i],mx);
}
signed main(){
memset(h,-0x3f,sizeof(h));
n=read();
for(int i=1;i<=n;i++)T[i]=read(),s[i]=s[i-1]+T[i];
m=read();
for(int i=1;i<=m;i++)P[i]=read(),X[i]=read();
int r=1;que[1]=0;//stressed
for(int i=1;i<=n;i++){
while(1<r&&1.0*(yf(que[r])-yf(que[r-1]))/(que[r]-que[r-1])<i)r--;
f[i]=max(f[i-1],f[que[r]]+(i-que[r])*(i-que[r]+1)/2-s[i]+s[que[r]]);
while(1<r&&1.0*(yf(que[r])-yf(que[r-1]))/(que[r]-que[r-1])<1.0*(yf(i)-yf(que[r]))/(i-que[r]))r--;
que[++r]=i;
}
r=1;que[1]=n+1;//stressed
for(int i=n;i;i--){
while(1<r&&1.0*(yg(que[r])-yg(que[r-1]))/(que[r]-que[r-1])>i)r--;
g[i]=max(g[i+1],g[que[r]]+(que[r]-i)*(que[r]-i+1)/2-s[que[r]-1]+s[i-1]);
while(1<r&&1.0*(yg(que[r])-yg(que[r-1]))/(que[r]-que[r-1])>1.0*(yg(i)-yg(que[r]))/(i-que[r]))r--;
que[++r]=i;
}
solve(1,n);
for(int i=1;i<=m;i++)cout<<max(h[P[i]]+T[P[i]]-X[i],f[P[i]-1]+g[P[i]+1])<<'\n';
}