斜率优化

斜率优化是一种对于形如 \(F_{i}=\min/\max\{F_j+...\}\) 的转移方程式,将其结构变换从而与几何联系起来,转化为坐标系内求凸包切线的问题。

结合例题讲解。

【例1】任务安排 1

说明:题目中的 \(f\) 被我篡改成了 \(c\)

列出朴素转移方程。设 \(F(i,j)\) 表示前 \(i\) 物品分成 \(j\) 段的最小费用,\(C[i],T[i]\) 分别为 \(c_i,t_i\) 前缀和。

\[F(i,j)=\min_{0\le k<i}\{F(k,j-1)+(j*S+T[i])*(C[i]-C[k])\} \]

时间复杂度 \(O(n^3)\)

一个常用的技巧叫做 费用提前计算:考虑到

  1. 题目并未要求具体分几段,因此 \(j\) 维理论可以省掉
  2. \(k+1\sim i\) 多分成一段,造成的影响 是后面所有的物品完成时间 \(+S\)

因此,可以写出

\[F[i]=\min_{0\le j<i}\{ F[j]+(C[i]-C[j])*T[i]+S*(C[n]-C[j]) \]

相当于是把费用给在产生时就计算了。由于 ________________________(坑待填),这种优化方式是不会造成错误决策的。

时间复杂度 \(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\) 去掉。

\[F[i]= F[j]+(C[i]-C[j])*T[i]+S*(C[n]-C[j]) \]

再展开。

\[F[i]=F[j]+C[i]T[i]-C[j]T[i]+SC[n]-SC[j] \]

再将只关于 \(j\) 的、同时关于 \(i,j\) 的、常数项区分开。由于所在是 \(i\),因此含 \(i\) 的(例如 \(T[i],C[i]\))就等同不变量(常数)。

只关于 \(j\) 的:放在等号左边。

\[F[j]=C[j](S+T[i])+F[i]-C[i]T[i]-SC[n] \]

这样,可以看成一个一次函数 \(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)\)。在上一题中,我们将斜率的分母乘到了另一边,十字相乘了,但这样有两个条件:

  1. 分母是正数

  2. 不会爆 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 了。

\[F(w,i)=\min_{0\le j<i}\{F(w-1,j)+\sum_{k=j+1}^i((t_i-y_i)-(t_k-y_k))\} \]

展开得到

\[F(w,i)=F(w-1,j)+(i-j)*(t_i-y_i)-T[i]+T[j]+Y[i]-Y[j] \]

整理得

\[F(w-1,j)+T[j]-Y[j]=(t_i-y_i)*j+F(w,i)-(t_i-y_i)*i+T[i]-Y[i] \]

就可以斜率优化了。这时再看重载运算符,发现刚好保证了斜率单调性,是不是很巧妙呢?

这题一定不能把分母乘到另一边因为两者都不满足。

#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';
}
posted @ 2022-02-09 19:49  pengyule  阅读(89)  评论(0编辑  收藏  举报