[复习]斜率优化
好久没写过了,跟忘了没啥区别了。
然后重新理解一遍这个东西,感觉我原来对于斜率优化的想法有着很大的问题。
所以这些东西举例子重新推一推吧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
#include
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=Calc(i,Q[h+1]))++h;
f[i]=Calc(i,Q[h]);
while(h
[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
#include
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=Slope(Q[t-1],Q[t]))--t;
Q[++t]=i;
}
printf("%lld\n",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
#include
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\n",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
#include
#include
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
第二种方法是平衡树,用平衡树维护这个凸壳,每个节点按照\(x\)轴排序,每次插入一个新点的时候先检查这个点是否存在于凸壳内,如果存在,把两侧不合法的点全部删掉。
这个东西就是动态维护凸壳而已,似乎和单纯的斜率优化关系不是很大了。
#include
#include
#include
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])=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
Ending
似乎就这么多了QwQ
总结一下,其实推式子都是大同小异的。
主要的区别就在于坐标是否具有单调性。
如果坐标和询问的斜率都单调,直接使用单调队列就好了。
如果坐标单调而询问的斜率不单调,那么就需要维护单调栈,在其中二分斜率。
如果坐标不单调,那么用\(CDQ\)分治或者\(Splay\)维护凸壳(二进制分组多一个\(log\))。
upd:还可以用李超线段树来维护。
除了这几道裸的\(dp\)题之外,显然还是有很多套路的,比如说套上线段树分治让你维护凸壳之类的,这里都懒得写了。
就这样啦QaQ。