一轮集训DAY4斜率优化入门

本文不讨论CDQ,平衡树维护凸包

斜率优化入门

我们讨论类似于以下的DP转移式(max同理)

fi=min{fj+g(i)+h(j)+a(i)b(j)}

比较两个决策j,k,设j>k且优于k

得到不等式:

fj+h(j)+a(i)b(j)<fk+h(k)+a(i)b(k)

移项变式得到:fj+h(j)(fk+h(k))<a(i)(b(j)b(k))

Y(n)=fn+h(n),X(n)=b(n)

此时根据X(j)X(k)的正负性进行变式,这里设X单调递增,变式可以得到:

Y(j)Y(k)X(j)X(k)<a(i)

这是j优于k的条件。类似地,反着来可以得到:Y(j)Y(k)X(j)X(k)a(i)的时候k优于j

我们这里设K(n)=Y(n)X(n),我们目前只讨论a(i)递增,X递增的情况

这里写一个引理:我们设一条斜率为a(i)的直线,不断向上平移,第一个遇到的点就是最优决策点。

证明:注意到Y(i)=fi+h(i),所以最小化Y就可以最小化fh是常数),故我们实际上需要考虑的就是最小化直线截距。而回到之前的条件:Y(j)Y(k)X(j)X(k)<a(i)表示j优于k,反之k优于j。我们设当前平面上插入的点集是S,则实际上对于最优决策点k而言,所有X坐标小于X(k)的点与点k所在的直线斜率小于a(i),右边的都大于等于a(i)(等于的原因是此时两个点都最优)。而我们向上平移时遇到的第一个点就满足这个性质且截距最小(如果不能理解随便画几组就能理解了)

考虑下面这种情况:

image-20230112090005086

由于我们只讨论a(i)递增的情况,不妨画几个直线看看规律:

image-20230112090329373

容易发现,此时的情况是先与C相交

再者:

image-20230112090538794

此时先与A相交更优

通过观察,不难发现,如果直线l满足kl<kAC,则与C相交,若kl>KAC则与A相交,等于时A,C等价。总之,不可能与B相交。

所以说B是无用的决策点,也即

image-20230112090755469

这种上凸的情况可以删去B,仅保留AC即可。

推而广之,可以发现我们维护的是这样的一个凸包:

image-20230112091014822

而不难发现的是,如果a(i),X都是单调的,真正的最优决策点会呈现出这个样子:

image-20230112091339271

此时A,B,C,D,E都有可能成为最优决策,而F,G是无用的,可以大胆删去。由此我们可以使用单调队列维护这个凸包的一半,每次取队头即为最优决策

示例代码:

	q[++t]=0;
	for(int i=1;i<=n;i++){
		while(h<t&&s[i]*down(q[h+1],q[h])>up(q[h+1],q[h]))h++;
		get(i,q[h]);
		while(h<t&&up(i,q[t])*down(q[t],q[t-1])<up(q[t],q[t-1])*down(i,q[t]))t--;
		q[++t]=i;
	}
	cout<<f[n]<<endl;
}

实现时注意一个点,优先插入一个点(0,0),只有单调队列里有超过两个元素的时候才能够进行比较决策。

这使得我们引出一个问题:如果a(i)不单调呢?

很简单,就是F,G也可能成为最优决策。

这时候怎么办?我们只能用单调队列维护凸包的下凸性质,而不能高效查找这个最优决策点,怎么办呢?

回想起之前的引理:

Y(j)Y(k)X(j)X(k)<a(i) 这是j优于k的条件。类似地,反着来可以得到:Y(j)Y(k)X(j)X(k)a(i)的时候k优于j

那么,设按照x坐标从小到大,凸包点集为S,由于单调队列里下凸包斜率有序,可以二分查找使得(Sk,Sk1)k<a(i)(Sk,Sk+1)的点Sk

注意边界的判断。

示例代码:

int find(int i,int k){
	if(l==r)return q[l];
	int L=l,R=r;
	while(L<R){
		int mid=(L+R)>>1;
		if(down(q[mid+1],q[mid])<=k*up(q[mid+1],q[mid]))L=mid+1; 
		else R=mid;
	}
	return q[L];
}
//以下单调队列部分
	l=r=1;
	for(int i=1;i<=n;i++){
		int p=find(i,s+T[i]);
		f[i]=f[p]-(s+T[i])*C[p]+T[i]*C[i]+s*C[n];
		while(l<r&&down(q[r],q[r-1])*up(i,q[r])>=down(i,q[r])*up(q[r],q[r-1]))r--;
		q[++r]=i;
	}

例题

T1

土地购买

考虑一种贪心,我们将每一块土地的左下角都放在坐标系的原点,容易发现,如果存在点A(x1,y1),B(x2,y2),如果x1x2,y1y2A,B肯定在同一组,此时A肯定不会有任何贡献。所以最后需要考虑的点集是这个样子:

image-20230112100820066

我们需要找到若干个矩形,使得所有点都被覆盖,求矩形面积之和的最小值。将所有点的x从小到大排序,则y递减

fi表示覆盖前i个点所需要的最小代价,容易写出转移方程式:

fi=min{fj+xi×yj+1}

比较两个决策k<j<i,设j优于k,得到:

fj+xi×yj+1<fk+xi×yk+1

X(n)=yn+1,Y(n)=fn,则容易得到:

Y(j)Y(k)X(j)X(k)>xi

所以实际需要维护的是一个上凸壳。并且xi单调递减。

#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
#define N 500500
#define M 1050050
struct node{
	int x,y;
}a[N],b[N];
int h=1,t=1,c[M+500],cnt[N],n,m,tot,f[N],x[N],y[N],q[N];
bool cmp(node a,node b){
	return a.x==b.x?a.y<b.y:a.x>b.x;//这里是按y从小到大哦
}
#define lowbit(x) x&-x
void add(int x,int k){
	while(x<M){
		c[x]+=k;x+=lowbit(x);	
	}
}
int ask(int x){
	int ans=0;
	while(x){
		ans+=c[x];x-=lowbit(x); 
	}
	return ans;
}
void init(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i].x>>a[i].y;
	sort(a+1,a+n+1,cmp);
	add(a[1].y,1);
	for(int i=2;i<=n;++i){
		cnt[i]=i-1-ask(a[i].y-1);//这里起到了去重的作用,不-1会WA#7
		add(a[i].y,1);
	}
	for(int i=1;i<=n;i++)if(!cnt[i])b[++tot]=a[i];
	reverse(b+1,b+tot+1);//翻过来
	for(int i=1;i<=tot;i++)x[i]=b[i].x,y[i]=b[i].y;
	//cout<<endl;
	//for(int i=1;i<=tot;i++)cout<<x[i]<<" "<<y[i]<<endl;
}
int X(int x){
	return y[x+1];
}
int Y(int s){
	return f[s];
}
int up(int i,int j){
	return Y(i)-Y(j);
}
int down(int i,int j){
	return X(i)-X(j); 
}
long double slope(int i,int j){
	return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);//必须特判
}
void get(int s,int k){
	f[s]=f[k]+x[s]*y[k+1];
}
signed main(){
	ios::sync_with_stdio(false);
	init();
	for(int i=1;i<=tot;i++){
		while(h<t&&slope(q[h],q[h+1])>=-1.0*x[i])h++;//记得乘-1
		get(i,q[h]);
		while(h<t&&slope(q[t],q[t-1])<=slope(i,q[t]))t--;
		q[++t]=i;
	}
	cout<<f[tot];
}

T2

仓库建设

fi表示前i个工厂中在第i个工厂建立仓库的最小代价

容易得知:

fi=min{fj+ci+k=j+1ipk(xixk)

对其进行前缀和优化,设Si=k=1ipi,Ti=k=1ixipi

带入并抽离与i有关的项,得到:

fi=min{fj+TjxiSj}TixiSi+ci

比较决策j,k,设j>kj优于k

fj+TjxiSj<fk+TkxiSkfj+Tj(fk+Tk)<xi(SjSk)

j>kSj>Sk

所以设Y(n)=fn+Tn,X(n)=Sn

就可以得到:

Y(j)Y(k)X(j)X(k)<xi

维护一个下凸壳,这里由于X,xi显然具有单调性,所以用单调队列维护即可。

注意一个坑点是pi=0的时候可以不修,所以最后还要检查一下。

inline int X(int x){
	return s[x];
} 
inline int Y(int x){
	return f[x]+t[x];
}
inline int up(int i,int j){
	return Y(i)-Y(j);
}
inline int down(int i,int j){
	return X(i)-X(j);
}
inline long double slope(int i,int j){
	return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
inline void get(int w,int k){
	f[w]=f[k]+c[w]-t[w]+t[k]+x[w]*s[w]-x[w]*s[k];
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>x[i]>>p[i]>>c[i];
		s[i]=s[i-1]+p[i];t[i]=t[i-1]+x[i]*p[i]; 
	}
	for(int i=1;i<=n;i++){
		while(h<ta&&slope(q[h],q[h+1])<=x[i])h++;
		get(i,q[h]);
		while(h<ta&&slope(i,q[ta])<=slope(q[ta],q[ta-1]))ta--;
		q[++ta]=i;
	}
	int sit=n;
	while(p[sit]==0)sit--;
	int ans=f[n];
	for(int k=sit;k<=n;k++)ans=min(ans,f[k]);
	cout<<ans<<endl;
}

T3

Cats Transport

Si=k=1iDi,则若从时刻0出发,接到第i只猫的时间就为:TiSHi。这个式子进行加减某个值x,就可以得到从x时刻出发,猫所等待的时间了,故设Qi=TiSHi

显然在最优策略里面,P个饲养员肯定都是各自选择了一只猫,刚好接走这只猫。所以不妨将Qi递增排序,此时若选择刚好带走Qk,那么就可以顺带带走Q1Qk1。因为这些先耍完等着铲屎官了。这里就凸显一个贪心策略:每个饲养员肯定都是选择其中一段全部带走。设一次性带走区间[l,r]的猫,则肯定是从Qr时刻出发,在0时刻恰好接走r。则总的等待时间为:i=lr(QrQi)

这个问题等价于:在一段单调不降的序列中,将其分为p个连续区间,每个区间的代价为此区间的最大值乘以区间长度并减去此区间权值和,要求最小化每个区间的代价和。

对其进行前缀和优化,不妨设Ai=k=1iQk,设fi,j表示将前i个数分为j段的最小代价,则容易得到状态转移方程:

fi,j=mink<j{fi1,kAj+Ak+(jk)Qj}

对其内层进行斜率优化,比较决策k1,k2,设k1>k2k1优于k2

则得到:

fi1,k1+Ak1k1Qj<fi1,k2+Ak2k2Qj

Y(n)=fi1,n+An,X(n)=n

则得到:

Y(k1)Y(k2)Xk1Xk2<Qj

显然Q,X都具备单调性,所以使用单调队列维护,每一次i的变化将队列清空即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long 
#define N 100500
#define P 150
int f[P][N],D[N],q[N],Q[N],S[N],A[N],H[N],T[N],n,m,h,t,p;
inline int X(int x,int t){
	return x;
}
inline int Y(int x,int i){
	return f[i-1][x]+A[x];
}
inline int up(int t,int i,int j){
	return Y(i,t)-Y(j,t);
}
inline int down(int t,int i,int j){
	return X(i,t)-X(j,t);
}
inline long double slope(int t,int i,int j){
	return down(t,i,j)==0?1e18:1.0*up(t,i,j)/down(t,i,j);
}
inline void get(int i,int j,int k){
	f[i][j]=f[i-1][k]-A[j]+A[k]+(j-k)*Q[j];
}
void init(){
	cin>>n>>m>>p;
	for(int i=1;i<=m;i++)f[0][i]=0x3f3f3f3f3f3f;
	f[0][0]=0;
	for(int i=2;i<=n;i++){
		cin>>D[i];
		S[i]=D[i]+S[i-1]; 
	}
	for(int i=1;i<=m;i++){
		cin>>H[i]>>T[i];
		Q[i]=T[i]-S[H[i]];
	}
	sort(Q+1,Q+m+1);
	for(int i=1;i<=m;i++){
		A[i]=A[i-1]+Q[i];
	}
}
void solve(int i){
	h=t=1;q[1]=0;
	for(int j=1;j<=m;j++){
		while(h<t&&slope(i,q[h],q[h+1])<=Q[j])h++;
		get(i,j,q[h]);
		while(h<t&&slope(i,j,q[t])<=slope(i,q[t],q[t-1]))t--;
		q[++t]=j;
	}
} 
signed main(){
	ios::sync_with_stdio(false);
	init();
	for(int i=1;i<=p;i++)solve(i);
	cout<<f[p][m]<<endl;
}

T4

柠檬

引理:选择的每一段贝壳,左右端点大小相等。

证明:考虑反证法,当这段贝壳的s0选择的是端点之一的话,如果大小不等,可以往回缩到大小相等的一个端点,此时回缩的那段区间可以有更多的贡献。如果不是端点之一的话,可以从两端缩,原贡献不变,且回缩区间有更大贡献。

所以,设fi表示前i个贝壳里分段的最大贡献,预处理ti=k=1i[sk=si],则:

fi=max1ji,sj=si{fj1+(titj+1)2si}

拆开括号得到:

fi=max1ji,sj=si{fj1+ti2+(tj1)22ti(tj1)si}

所以比较决策j>kj优于k,容易得到:

fj1+(tj1)22ti(tj1)sj>fk1+(tk1)22ti(tk1)sk

Y(n)=fn1+(tn1)2sn,X(n)=sn(tn1)

容易得到:

Y(j)Y(k)X(j)X(k)>2ti

大于号维护上凸包。

注意到求max要使得截距最大化,容易发现随着斜率的增加,最优决策点也在不断向左移动,所以我们维护凸包的一半(斜率>2ti的部分),取最后一个合法点。类似于下图:

image-20230112195134680

所以我们需要维护一个单调结构,但只是从最后插入从最后取决策,所以应该使用单调栈进行维护。注意我们是对每一个值都开一个单调栈维护。

Code:

#include<stack>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
#define N 105000
#define ll long long
int t[N],s[N],n,m,pre[N];
ll f[N];
stack<int>q[N];
inline ll X(int x){
	return 1ll*s[x]*t[x]-1ll*s[x];
}
inline ll Y(int x){
	return 1ll*f[x-1]+1ll*(t[x]-1)*(t[x]-1)*s[x];
}
inline ll up(int i,int j){
	return Y(i)-Y(j);
}
inline ll down(int i,int j){
	return X(i)-X(j);
}
inline long double slope(int i,int j){
	return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
inline void get(int i,int j){
	//cout<<i<<" "<<j<<endl; 
	if(!j){f[i]=s[i];return ;} 
	f[i]=1ll*f[j-1]+1ll*(t[i]-t[j]+1)*(t[i]-t[j]+1)*s[i];
	//cout<<f[j-1]<<" "<<(t[i]-t[j]+1)<<" "<<s[i]<<endl; 
}
void read(int &x){
	x=0;char ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar(); 
} 
void init(){
	read(n);
	for(int i=1;i<=n;i++)read(s[i]);
	for(int i=1;i<=n;i++)t[i]=t[pre[s[i]]]+1,pre[s[i]]=i;
	for(int i=1;i<=n;i++)if(q[s[i]].empty())q[s[i]].push(0);
}
signed main(){
	ios::sync_with_stdio(false);
	init();
	for(int i=1;i<=n;i++){
		while(q[s[i]].size()>1){
			int x=q[s[i]].top();q[s[i]].pop();
			int y=q[s[i]].top();
			if(slope(x,y)>slope(i,x)){
				q[s[i]].push(x);break;
			}
		}
		q[s[i]].push(i);
		while(q[s[i]].size()>1){
			int x=q[s[i]].top();q[s[i]].pop();
			int y=q[s[i]].top();
			if(slope(x,y)>t[i]*2){
				q[s[i]].push(x);break;
			}
		} 
		get(i,q[s[i]].top());
	}
	//for(int i=1;i<=n;i++)cout<<f[i]<<" ";
	cout<<f[n];
}

步骤总结

首先得写出状态转移方程,类似于:

fi=min{fj+g(i)+h(j)+a(i)b(j)}

然后比较两个决策j,k,设j>k且优于k,列出不等式(max的时候就是>):

fj+g(i)+h(j)+a(i)b(j)<fk+g(i)+h(k)+a(i)b(k)

整理后可以得到一个斜率式:

Y(n)=fn+h(n),X(n)=b(n)

此时根据X(j)X(k)的正负性进行变式,这里设X单调递增,变式可以得到:

Y(j)Y(k)X(j)X(k)a(i)进行比较,此时如果连接的是<,则需要维护下凸壳,>就是上凸壳

然后根据实际情况,我们可以分为:

  1. a(i)X都具备单调性,此时使用单调队列维护即可(事实上我们仅需维护凸包的一半),每次取队头作为最优决策
  2. a(i)不具备单调性,X仍然具有,此时仍然使用单调队列维护凸包,但最优决策需要二分查找
  3. 如果二者都不单调,需要使用平衡树/CDQ分治动态插入。(较为高深,俺不会)。
  4. 需要注意的是,根据实际情况,从尾部取决策可能会考虑使用单调栈进行维护
posted @   spdarkle  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示