[復習]斜率優化


[復習]斜率優化

好久沒寫過了,跟忘了沒啥區別了。
然后重新理解一遍這個東西,感覺我原來對於斜率優化的想法有着很大的問題。
所以這些東西舉例子重新推一推吧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\n",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\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<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\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<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\n",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\n",f[n]);
	return 0;
}

Ending

似乎就這么多了QwQ
總結一下,其實推式子都是大同小異的。
主要的區別就在於坐標是否具有單調性。
如果坐標和詢問的斜率都單調,直接使用單調隊列就好了。
如果坐標單調而詢問的斜率不單調,那么就需要維護單調棧,在其中二分斜率。
如果坐標不單調,那么用\(CDQ\)分治或者\(Splay\)維護凸殼(二進制分組多一個\(log\))。
upd:還可以用李超線段樹來維護。

除了這幾道裸的\(dp\)題之外,顯然還是有很多套路的,比如說套上線段樹分治讓你維護凸殼之類的,這里都懶得寫了。
就這樣啦QaQ。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM