[复习]斜率优化

[复习]斜率优化

好久没写过了,跟忘了没啥区别了。
然后重新理解一遍这个东西,感觉我原来对于斜率优化的想法有着很大的问题。
所以这些东西举例子重新推一推吧QwQ。

[HNOI2010]玩具装箱

首先写暴力(O(n^2))的转移,设(S_i)(C_i)的前缀和。

[f[i]=min_{j=0}^{i-1}f[j]+(i-j-1+S_i-S_j-L)^2 ]

[f[i]=min_{j=0}^{i-1}f[j]+(i-j-1)^2+(S_i-S_j-L)^2+2(i-j-1)(S_i-S_j-L) ]

然后把式子拆开,和(j)无关的直接移出去,只和(j)相关的放在一起,同时和(i,j)相关的放在一起。
那么分类之后就是这样的:

  • (j)无关的:(i^2-2i+1+S_i^2+L^2-2LS_i+2iS_i-2iL-2S_i+2L)
  • 只和(j)有关的:(f[j]+j^2+2j+S_{j}^2+2LS_j+2jS_j+2jL+2S_j)
  • 同时和(i,j)相关的:(-2ij-2S_iS_j-2iS_j-2jS_i=-2(i+S_i)(j+S_j))

一共(22)项,似乎没有什么问题。(其实可以直接令(M_i=i-1+S_i-L),但是为了锻炼拆式子能力就这样吧......算了,我编不下去了.....)
那么把和(j)无关的部分记做(pre[i]),只和(j)有关的记做(y[j])(j+S_j)记做(x[j])(2(i+S_i))记做(k_i)
那么转移方程可以改写成:

[f[i]=pre[i]+min_{j=0}^{i-1}(-k_ix[j]+y[j]) ]

(k_i)是一个常数,我们把后面这个式子理解为一个一次函数(y=kx+b)的形式,得到(b=y-kx)
什么意思呢?平面上有若干个点((x[j],y[j])),你要过这些点画一条斜率为(k_i)的直线,使得其截距最小。
不难发现满足条件的(j)一定在下凸壳上。
这里有个很优秀的性质,也就是(k_i,x[j])都是单增的。
这样子凸壳可以直接用单调队列维护,而取最优值只需要每次找到凸壳左侧最优位置就好啦。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define MAX 50050
inline int read()
{
	int x=0;bool t=false;char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
	if(ch=='-')t=true,ch=getchar();
	while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
	return t?-x:x;
}
int n,c[MAX];
ll L,S[MAX],f[MAX],pre[MAX],x[MAX],y[MAX];
ll Sqr(ll x){return x*x;}
ll Calc(int i,int j){return f[j]+Sqr(i-j-1+S[i]-S[j]-L);}
int Q[MAX],h,t;
double Slope(int i,int j){return 1.0*((y[i]+f[i])-(y[j]+f[j]))/(x[i]-x[j]);}
int main()
{
	n=read();L=read();
	for(int i=1;i<=n;++i)c[i]=read(),S[i]=S[i-1]+c[i];
	for(int i=1;i<=n;++i)pre[i]=1ll*i*i-2*i+1+S[i]*S[i]+L*L-2*L*S[i]+2*i*S[i]-2*i*L-2*S[i]+2*L;
	for(int i=1;i<=n;++i)y[i]=1ll*i*i+2*i+S[i]*S[i]+2*L*S[i]+2*i*S[i]+2*i*L+2*S[i];
	for(int i=1;i<=n;++i)x[i]=i+S[i];
	f[0]=0;Q[h=t=1]=0;
	for(int i=1;i<=n;++i)
	{
		while(h<t&&Calc(i,Q[h])>=Calc(i,Q[h+1]))++h;
		f[i]=Calc(i,Q[h]);
		while(h<t&&Slope(Q[t-1],i)<=Slope(Q[t-1],Q[t]))--t;
		Q[++t]=i;
	}
	printf("%lld
",f[n]);
	return 0;
}

[APIO2010]特别行动队

首先还是可以写出(O(n^2))的暴力(dp)

[f[i]=max_{j=0}^{i-1}f[j]+a(S_i-S_j)^2+b(S_i-S_j)+c ]

还是把式子拆开之后按照前面的三类分开。

  • (j)无关:(aS_i^2+bS_i+c)
  • 只与(j)有关:(f[j]+aS_j^2-bS_j)
  • (i,j)有关:(-2aS_iS_j)

还是和前面一样,与(j)无关记做(pre[i]),只与(j)有关记做(y[j]),同时有关的剔除与(i)有关的部分之后就是(x[j]=S_j)
转移改写成

[f[i]=pre[i]+max_{j=0}^{i-1}y[j]-2aS_ix[j] ]

似乎又是前面那种给定点和斜率求最大值截距的问题了。
这次是取最大值,且因为(a<0),所以(-2aS_i)单增,因此维护的是上凸壳。
并且(x[j])单调,所以可以直接拿单调队列维护。

#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 1000100
#define ll long long
inline int read()
{
	int x=0;bool t=false;char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
	if(ch=='-')t=true,ch=getchar();
	while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
	return t?-x:x;
}
int n;ll A,B,C,f[MAX],S[MAX],x[MAX],y[MAX];
double Slope(int a,int b){return 1.0*((y[a]+f[a])-(y[b]+f[b]))/(x[a]-x[b]);}
ll Calc(int i,int j){return f[j]+A*(S[i]-S[j])*(S[i]-S[j])+B*(S[i]-S[j])+C;}
int Q[MAX],h,t;
int main()
{
	n=read();A=read();B=read();C=read();
	for(int i=1;i<=n;++i)S[i]=S[i-1]+read();
	for(int i=1;i<=n;++i)y[i]=A*S[i]*S[i]-B*S[i];
	for(int i=1;i<=n;++i)x[i]=S[i];
	Q[h=t=1]=0;
	for(int i=1;i<=n;++i)
	{
		while(h<t&&Calc(i,Q[h])<=Calc(i,Q[h+1]))++h;
		f[i]=Calc(i,Q[h]);
		while(h<t&&Slope(Q[t-1],i)>=Slope(Q[t-1],Q[t]))--t;
		Q[++t]=i;
	}
	printf("%lld
",f[n]);
	return 0;
}

[SDOI2012]任务安排

考虑(O(N^2))暴力(dp),显然每次把后面所有的费用在当前的时间中的贡献一起计算。
(SF[i])(F[i])的后缀和,(ST[i])(T[i])的前缀和,得到转移:

[f[i]=min_{j=0}^{i-1}f[j]+SF[j+1]*(S+ST[i]-ST[j]) ]

假装(SF)数组被向左平移了一位,这样子也可以看做只和(j)相关的部分。
然后来分类:

  • (j)无关:似乎并没有
  • 只和(j)相关:(f[j]+SF[j+1]*(S-ST[j]))
  • (i,j)相关:(SF[j+1]*ST[i])

然后就可以很套路的令(y[j]=f[j]+SF[j+1]*(S-ST[j])),然后(x[j]=SF[j+1]),然后每次就可以看做在斜率(-ST[i])上询问最小截距?
看起来很美好,但是我们发现了一个问题,(T)这个东西可以是负数,所以(ST)并不是单调的,所以不能直接用单调队列来维护这个东西。而且(x[j])这玩意是从右往左单减的,所以凸包还要反着维护。
那么凸壳还是可以直接维护的,只是每次询问的斜率不单调而已,所以我们只需要每次在凸壳上二分这个斜率就好了。
注意一下这里因为要求的是截距的最小值,且斜率为负,所以需要求的是一个下凸壳。
这里为了防止掉精度(比如(Delta x=0)这样子)所以斜率的比较全部都乘过去变成乘积的比较。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define MAX 300300
inline int read()
{
	int x=0;bool t=false;char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
	if(ch=='-')t=true,ch=getchar();
	while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
	return t?-x:x;
}
int n;
ll T[MAX],F[MAX],S,f[MAX],x[MAX],y[MAX];
ll Calc(int i,int j){return f[j]+F[j+1]*(S+T[i]-T[j]);}
bool chk1(int i,int j,int k){return 1.0*((y[i]+f[i])-(y[j]+f[j]))*(x[i]-x[k])<=1.0*((y[i]+f[i])-(y[k]+f[k]))*(x[i]-x[j]);}
bool chk2(int i,int j,ll k){return 1.0*(y[i]+f[i])-(y[j]+f[j])<=1.0*k*(x[i]-x[j]);}
int Q[MAX],top;
int Bound(ll k)
{
	int l=1,r=top-1,ret=top;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(chk2(Q[mid],Q[mid+1],k))r=mid-1,ret=mid;
		else l=mid+1;
	}
	return Q[ret];
}
int main()
{
	n=read();S=read();
	for(int i=1;i<=n;++i)T[i]=read(),F[i]=read();
	for(int i=n;i>=0;--i)F[i]+=F[i+1];
	for(int i=1;i<=n;++i)T[i]+=T[i-1];
	for(int i=0;i<=n;++i)y[i]=F[i+1]*(S-T[i]);
	for(int i=0;i<=n;++i)x[i]=F[i+1];
	Q[top=1]=0;
	for(int i=1;i<=n;++i)
	{
		f[i]=Calc(i,Bound(-T[i]));
		while(top>1&&chk1(Q[top-1],Q[top],i))--top;
		Q[++top]=i;
	}
	printf("%lld
",f[n]);
	return 0;
}

[NOI2007]货币兑换

首先题目里的提示已经告诉我们每次要么花完所有钱,要么卖出所有金券。
我们设(f[i])表示第(i)天结束时能够得到的最大价值的钱,考虑这个东西怎么转移。那么我们枚举上一次买入金券是哪一天,把那一天结束时能够得到的钱全部换成金券,然后到当前第(i)天卖掉,那么我们就可以写出一个(O(n^2))的暴力了。
(VB[j]=f[j]/(SA[j]*Rate[j]+SB[j])),得到转移:

[f[i]=max_{j=0}^{i-1}SA[i]*Rate[j]*VB[j]+SB[i]*VB[j] ]

其中(SA[i])表示第(i)天金券(A)的价值,(SB[i])同理,(Rate[i])同题面的含义。
类似前面分类:
(j)无关:似乎并没有
之和(j)有关:似乎也没有
同时和(i,j)有关:似乎都是的。
看起来似乎并不好写成斜率的形式了?
直接令(x[j]=VB[j]*Rate[j],y[j]=VB[j]),那么转移可以写成:

[f[i]=max_{j=0}^{i-1}SA[i]*x[j]+SB[i]*y[j] ]

然后强行把其中一项(i)给提出来,变成:

[f[i]=SB[i]*max_{j=0}^{i-1}frac{SA[i]}{SB[i]}*x[j]+y[j] ]

于是问题又变成了斜率的问题,即给定一对点((x[j],y[j])),求用斜率(-frac{SA[i]}{SB[i]})的直线穿过这些点之后的最大截距。
于是我们就要来维护一个上凸壳。
那么问题来了,(x[j])完全不单调,应该怎么维护呢?
方法有两种,第一种是用(CDQ)分治处理,另外一种是用(Splay)动态维护凸壳。当然,你如果愿意也可以使用二进制分组来强行把(CDQ)给在线化,这里就不讨论这个东西。
先说第一种。
首先明白这样一个事实:我们斜率优化并不是只能从一个凸包上取答案,我们可以把所有的点分组建立凸包,分别在每个凸包上询问取最优值。那么(CDQ)分治就可以很好的处理这个问题。
因为我们在询问一个点之前,它前面的所有点的值都必须算出,所以(CDQ)的步骤是:递归处理左侧,处理当前整个区间,处理右侧,这个顺序不要弄错。
这样子只需要给左半边的所有点排序构建一个上凸壳,然后给右侧的所有点依次二分做询问就好了。
注意这里写的时候不要忘记还有(f[i]=max{f[i],f[i-1],f[0]})这个转移了。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAX 100100
int n;double f[MAX];
double SA[MAX],SB[MAX],Rate[MAX];
double Calc(int i,int j)
{
	double B=f[j]/(SA[j]*Rate[j]+SB[j]);
	double A=Rate[j]*B;
	return SA[i]*A+SB[i]*B;
}
struct Node{double x,y;int id;}p[MAX],tmp[MAX];
int Q[MAX],top;
double Slope(int i,int j)
{
	if(fabs(p[i].x-p[j].x)<1e-9)return 1e-15;
	return (p[i].y-p[j].y)/(p[i].x-p[j].x);
}
int Bound(double K)
{
	int l=1,r=top-1,ret=top;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(Slope(Q[mid],Q[mid+1])<=K)r=mid-1,ret=mid;
		else l=mid+1;
	}
	return Q[ret];
}
void CDQ(int l,int r)
{
	if(l==r)
	{
		f[l]=max(f[l],f[l-1]);p[l].id=l;
		p[l].y=f[l]/(SA[l]*Rate[l]+SB[l]);
		p[l].x=p[l].y*Rate[l];
		return;
	}
	int mid=(l+r)>>1;CDQ(l,mid);top=0;
	for(int i=l;i<=mid;++i)
	{
		while(top>1&&Slope(Q[top-1],Q[top])<=Slope(Q[top-1],i))--top;
		Q[++top]=i;
	}
	for(int i=mid+1;i<=r;++i)f[i]=max(f[i],Calc(i,p[Bound(-SA[i]/SB[i])].id));
	CDQ(mid+1,r);int t1=l,t2=mid+1,t=l;
	for(;t1<=mid||t2<=r;)
		if(t1<=mid&&(t2>r||(p[t1].x<p[t2].x)||(fabs(p[t1].x-p[t2].x)<1e-9&&p[t1].y<p[t2].y)))tmp[t++]=p[t1++];
		else tmp[t++]=p[t2++];
	for(int i=l;i<=r;++i)p[i]=tmp[i];
}
int main()
{
	scanf("%d%lf",&n,&f[0]);
	for(int i=1;i<=n;++i)scanf("%lf%lf%lf",&SA[i],&SB[i],&Rate[i]);
	for(int i=1;i<=n;++i)f[i]=f[0];
	CDQ(1,n);printf("%.3lf
",f[n]);
	return 0;
}

第二种方法是平衡树,用平衡树维护这个凸壳,每个节点按照(x)轴排序,每次插入一个新点的时候先检查这个点是否存在于凸壳内,如果存在,把两侧不合法的点全部删掉。
这个东西就是动态维护凸壳而已,似乎和单纯的斜率优化关系不是很大了。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAX 100100
const double inf=1e9;
const double eps=1e-7;
inline int read()
{
	int x=0;bool t=false;char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
	if(ch=='-')t=true,ch=getchar();
	while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
	return t?-x:x;
}
int n;
double f[MAX],x[MAX],y[MAX];
double SA[MAX],SB[MAX],Rate[MAX];
double Calc(int i,int j)
{
    double B=f[j]/(SA[j]*Rate[j]+SB[j]);
    double A=Rate[j]*B;
    return SA[i]*A+SB[i]*B;
}
double Slope(int i,int j)
{
	if(fabs(x[i]-x[j])<eps)return -1e20;
	return (y[i]-y[j])/(x[i]-x[j]);
}
struct SplayTree
{
#define ls (t[x].ch[0])
#define rs (t[x].ch[1])
	int root,tot;
	struct Node{int ch[2],ff;double k1,k2;}t[MAX];
	void rotate(int x)
	{
		int y=t[x].ff,z=t[y].ff;
		int k=t[y].ch[1]==x;
		if(z)t[z].ch[t[z].ch[1]==y]=x;t[x].ff=z;
		t[y].ch[k]=t[x].ch[k^1];if(t[x].ch[k^1])t[t[x].ch[k^1]].ff=y;
		t[x].ch[k^1]=y;t[y].ff=x;
	}
	void Splay(int x,int goal)
	{
		while(t[x].ff!=goal)
		{
			int y=t[x].ff,z=t[y].ff;
			if(z!=goal)
				(t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
			rotate(x);
		}
		if(!goal)root=x;
	}
	void Insert(int u)
	{
		if(!root){root=u;return;}
		int nw=root;
		while(233)
		{
			int p=x[nw]+eps<=x[u];
			if(!t[nw].ch[p]){t[nw].ch[p]=u;t[u].ff=nw;break;}
			else nw=t[nw].ch[p];
		}		
	}
	int Pre(int x)
	{
		Splay(x,0);int u=ls,ret=u;
		while(u)
			if(Slope(u,x)<=t[u].k1)ret=u,u=t[u].ch[1];
			else u=t[u].ch[0];
		return ret;
	}
	int Suf(int x)
	{
		Splay(x,0);int u=rs,ret=u;
		while(u)
			if(Slope(u,x)>=t[u].k2)ret=u,u=t[u].ch[0];
			else u=t[u].ch[1];
		return ret;
	}
	void Update(int x)
	{
		Splay(x,0);
		if(ls){int p=Pre(x);Splay(p,x);t[p].ch[1]=0;t[p].k2=t[x].k1=Slope(x,p);}
		else t[x].k1=inf;
		if(rs){int p=Suf(x);Splay(p,x);t[p].ch[0]=0;t[p].k1=t[x].k2=Slope(x,p);}
		else t[x].k2=-inf;
		if(t[x].k1<=t[x].k2)
			t[ls].ch[1]=rs,t[ls].ff=0,t[rs].ff=ls,root=ls,t[ls].k2=t[rs].k1=Slope(ls,rs);
	}
	int Bound(double K)
	{
		int x=root;
		while(233)
		{
			if(!x)return 0;
			if(t[x].k1>=K&&t[x].k2<=K)return x;
			if(t[x].k1<K)x=ls;else x=rs;
		}
	}
}T;
int main()
{
	scanf("%d%lf",&n,&f[0]);
    for(int i=1;i<=n;++i)scanf("%lf%lf%lf",&SA[i],&SB[i],&Rate[i]);
	for(int i=1;i<=n;++i)
	{
		int j=T.Bound(-SA[i]/SB[i]);
		f[i]=max(f[i-1],Calc(i,j));
		y[i]=f[i]/(SA[i]*Rate[i]+SB[i]);
		x[i]=y[i]*Rate[i];
		T.Insert(i);T.Update(i);
	}
	printf("%.3lf
",f[n]);
	return 0;
}

Ending

似乎就这么多了QwQ
总结一下,其实推式子都是大同小异的。
主要的区别就在于坐标是否具有单调性。
如果坐标和询问的斜率都单调,直接使用单调队列就好了。
如果坐标单调而询问的斜率不单调,那么就需要维护单调栈,在其中二分斜率。
如果坐标不单调,那么用(CDQ)分治或者(Splay)维护凸壳(二进制分组多一个(log))。
upd:还可以用李超线段树来维护。

除了这几道裸的(dp)题之外,显然还是有很多套路的,比如说套上线段树分治让你维护凸壳之类的,这里都懒得写了。
就这样啦QaQ。

原文地址:https://www.cnblogs.com/cjyyb/p/10549560.html