斜率优化dp小结

这个真的好容易啊

yxc小课

个人总结出斜率优化一般步骤:

  • 写出 Θ(n2)1d/1d 转移方程:即状态与转移都是一维的方程
  • 将dp式化为 Y=KX+B一次函数形式,尽可能保证斜率 K ,自变量 X 的单调性,将本轮答案 dp[i] 置于由常数只与 i 相关变量组成的截距 B 中。将转移用的 dp[j] 放置于因变量 Y 中。
  • 通过斜率与自变量的单调性维护凸包

例题

按照步骤,我们假设 j 是上一次设置特别行动队的右边界,j+1i 为本次特别行动队的区间,由题得方程:

dp[i]=maxj=0i1{dp[j]+a(k=j+1ixk)2+bk=j+1ixk+c}

检验正确性后进行如下操作:

  • 假设当前的 j 就是转移的最优解
  • 省略极值符号,将转移方程按照上文步骤拆解
  • 根据所得方程进行凸包维护

不妨使用前缀和维护士兵的初始战力和

dp[i]=dp[j]+a(sum[i]sum[j])2+b(sum[i]sum[j])+c

dp[i]=dp[j]+asum2[i]+asum2[j]2asum[i]sum[j]+bsum[i]bsum[j]+c

2sum[i]sum[j]+dp[i]asum2[i]bsum[i]c=dp[j]+asum2[j]bsum[j]

转化为 Y=KX+B 的形式:

Y=dp[j]+asum2[j]bsum[j]

K=2asum[i]

X=sum[j]

B=dp[i]asum2[i]bsum[i]c

注意到截距 B 是越大越好的,对于每个固定的 jY 是固定的,随着 i,j 的增长,K,X 是单调的。于是打开画图:

转移 dp[i] 时考虑扫描每个点

(X=sum[j],Y=dp[j]+asum2[j]bsum[j])

此时斜率固定,图上的黄点所对应的蓝点即截距 B 的最大值就是我们想要的答案。

因而我们注意到最下面的点不可能成为最优解,然而斜率 K 是单调递减的,经过反复试验我们得到了不同的 K 可以取到的答案区间:

保留的四个点代表了随着斜率的减小而取得的最优解。形成了一个上凸包

考虑如何取舍当前 K 的答案,连接这个凸包,并计算每一个线段的斜率以及取该点作为答案时的斜率即可发现:

观察得:斜率 K 的最优黄点是第一对斜率小于 K 的点对的第一个点

因此我们用单调队列维护斜率,由于当前斜率 K 的单调性易得:从前不够优秀的黄点以后也不可能成为最优解(*),因此整个转移是线性的,最优决策点 q[H] 一定会递增,这一点很重要,之后会有特殊情况。

通过刚才的斜率比对并修改队头,最优的下标即 q[h]

黄点,也就是当前已有的用来转移的点不是凭空出现的,当前转移的点 i 也要加入到队列中,并且这一加入可能会导致凸包的破坏:当前的线段比之前的队列中所存储的最不优的最优线段(队尾)更优,当且仅当队尾与 i 所连的线段的斜率大于队尾线段的斜率。很好理解,斜率尽可能大换来的是该点能解得的截距尽可能大。

翻译成线性规划:

此时 q[t] 的黄点已经不是最优了,保证斜率单调的情况下弹队尾,让i入队。

#include<bits/stdc++.h>
#define int long long
#define MAXN 1000005
using namespace std;
int n;
struct function{
	int a,b,c;
}f;
int sum[MAXN];
int dp[MAXN];
int q[MAXN],h,t;
int Y(int p){//根据上文分析出的X,Y,K等写出对应函数
	return dp[p]+f.a*sum[p]*sum[p]-f.b*sum[p];
}
int K(int p){
	return 2*f.a*sum[p];
}
double slope(int x1,int x2){
	return (Y(x1)-Y(x2))/(sum[x1]-sum[x2])*1.0;
}
signed main(){
	scanf("%lld",&n);
	scanf("%lld%lld%lld",&f.a,&f.b,&f.c);
	for(int i=1;i<=n;i++){
		scanf("%lld",&sum[i]);
		sum[i]+=sum[i-1];
	}
	for(int i=1;i<=n;i++){
		while(t>h&&slope(q[h],q[h+1])>K(i))++h;//找到第一个斜率小于i对应斜率的线段
		dp[i]=dp[q[h]]+f.a*(sum[i]*sum[i]-2*sum[i]*sum[q[h]]+sum[q[h]]*sum[q[h]])+f.b*(sum[i]-sum[q[h]])+f.c;//按照方程转移
		while(t>h&&slope(q[t],i)>=slope(q[t],q[t-1]))--t;//将不够优秀的点弹出,点i入队。
		q[++t]=i;
	}
	printf("%lld",dp[n]);
	return 0;
}

四张图的K是 2asum[i] ,wssb。

例题2

dp[i] 为装箱到第 i 个玩具时的最小费用,上一次装箱的右边界为 j

dp[i]=minj=0i1{dp[j]+(k=j+1ick+ij1L)2}

获得了一个癌症级别的多项展开式。

用前缀和替换并移项。

dp[i]=minj=0i1{dp[j]+((sum[i]+i)(sum[j]+j)(L+1))2}

不妨让 L +=1,重新定义 sum[i]k=1ick+i ,dp方程得以简化。

dp[i]=minj=0i1{dp[j]+(sum[i]sum[j]L)2}

然后开始拆解。

dp[i]=dp[j]+sum2[i]+sum2[j]+L22sum[i]sum[j]+2Lsum[j]2Lsum[i]

dp[j]+sum2[j]+2Lsum[j]=2sum[i]sum[j]+dp[i]+2Lsum[i]sum2[i]L2

转化为 Y=KX+B 的形式

Y=dp[j]+sum2[j]+2Lsum[j]

K=2sum[i]

X=sum[j]

B=dp[i]+2Lsum[i]sum2[i]L2

其中,KX 都单调递增。使用大脑法得出:维护下凸包。

同理,第一个大于第 i 个点斜率的线段的左端点就是最优解。

线性规划来看,一个点比之前的黄点更优,当且仅当该点在线段下方。

套板子即可。

#include<bits/stdc++.h>
#define int long long
#define MAXN 50005
using namespace std;
int n,l;
int w[MAXN];
int sum[MAXN];
int dp[MAXN];
int q[MAXN],h,t;
int Y(int x){
	return dp[x]+sum[x]*sum[x]+2*l*sum[x];
}
int X(int x){
	return sum[x];
}
double slope(int x_1,int x_2){
	return 1.0*(Y(x_1)-Y(x_2))/(X(x_1)-X(x_2));
}
signed main(){
	scanf("%lld%lld",&n,&l);
	++l;
	for(int i=1;i<=n;i++){
		scanf("%lld",&w[i]);
		sum[i]=sum[i-1]+w[i]+1;
	}
	for(int i=1;i<=n;i++){
		//dp[i]=min(dp[i],dp[j]+(sum[i]-sum[j]+(i-j-1)-l)*(sum[i]-sum[j]+(i-j-1)-l)); lsum
		//dp[i]=dp[j]+(sum[i]-sum[j]-l)(sum[i]-sum[j]-l)
		//dp[i]=dp[j]+(sum[i]-sum[j])2+l2-2l(sum[i]-sum[j])
		//dp[i]=dp[j]+sum2[i]+sum2[j]-2sum[i]sum[j]+l2-2lsum[i]+2lsum[j]
		//         Y            =   k   X     +             B
		//dp[j]+sum2[j]+2lsum[j]=2sum[i]sum[j]+dp[i]-sum2[i]+2lsum[i]-l2
		while(t>h&&slope(q[h],q[h+1])<=2.0*sum[i])++h;//找最优解
		dp[i]=dp[q[h]]+(sum[i]-sum[q[h]]-l)*(sum[i]-sum[q[h]]-l);//转移
		while(t>h&&slope(q[t-1],q[t])>=slope(q[t-1],i))--t;//更新答案
		q[++t]=i;
	}
	printf("%lld",dp[n]);
	return 0;
}

(*)性质是一个大前提,事实上这个大前提是可以不成立的。

例题3

先推 Θ(n2) 方程。

dp[i]=minj=0i1{dp[j]+(k=1itk+numS)k=j+1ick}

num 是已分的组数,问题是我们不能使用额外的维度来运行 dp。

dp[i]=minj=0i1{dp[j]+k=1itkk=j+1ick+(k=j+1ick)numS}

把方程拆开,借助题解发现方程的两部分可以分开处理。自 i 结束的每次分组影响的是 i+1n 的所有货物的处理时间,进而,dp[i] 对之后所有分组的代价影响 Cextra表示为:

Cextrai=(k=j+1nck)S

如果分组左端点下标组成的集合为TS 对最终的 dp[n] 的影响:

Cextra=iTk=i+1nckS

脑子不好想不来所以本人还画了一个图:

每个分组对总影响各取所需即可。但是我们只需要 dp[n] 的分组,此时的 Cextra 对其影响如图中 Group4 一样,所以 dp[n] 的额外代价为先前所有额外代价之和,我们在处理 dp[i] 时顺带计算 Cextrai 即可。

顺带转化为前缀和形式:

dp[i]=minj=0i1{dp[i],dp[j]+sumt[i](sumc[i]sumc[j])+S(sumc[n]sumc[j])}

然后按照之前的方法拆解。

dp[j]+S(sumc[n]sumc[j])=sumt[i]sumc[j]+dp[i]sumt[i]sumc[i]

Y=dp[j]+S(sumc[n]sumc[j])

K=sumt[i]

X=sumc[j]

B=dp[i]sumt[i]sumc[i]

尝试进一步分析时发现了问题:|Ti|<28 ,即 K=sumt[i] 不具备单调性。进而对 dp[i] 转移时的抉择不是线性复杂度。单调队列队头因此始终有利用可能,不得出队。

不过这不影响决策点的位置:第一个斜率大于 sumt[i] 的线段的左端点。

不过此时要用二分查找

当前点比原先黄点更优,当且仅当其于末位黄点下方。

#include<bits/stdc++.h>
#define int long long
#define MAXN 300005
using namespace std;
int n,s;
struct mission{
	int c,t;
}p[MAXN];
int csum[MAXN],tsum[MAXN];
int dp[MAXN];
int q[MAXN],h,t;
int Y(int x){
	return dp[x]-s*csum[x];
}
int X(int x){
	return csum[x];
}
double slope(int x_1,int x_2){
	return 1.0*(Y(x_1)-Y(x_2))/(X(x_1)-X(x_2));
}
int geth(int x){
	int l=h,r=t,res=r;//res始终没有更新说明该点的斜率是有史以来最大的,最优解为队尾。
	while(l<=r){
		int mid=l+r>>1;
		if(slope(q[mid+1],q[mid])>=(1.0*tsum[x]))r=mid-1,res=mid;
		else l=mid+1;	
	}
	return q[res];
}
signed main(){
	scanf("%lld%lld",&n,&s);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&p[i].t,&p[i].c);
		tsum[i]=tsum[i-1]+p[i].t;
		csum[i]=csum[i-1]+p[i].c;
	}
	for(int i=1;i<=n;i++){
		int loc=geth(i);
		dp[i]=dp[loc]+tsum[i]*(csum[i]-csum[loc])+s*(csum[n]-csum[loc]);
		while(t>h&&slope(i,q[t-1])<=slope(q[t],q[t-1]))--t;
		q[++t]=i;//正常处理
	}
	printf("%lld",dp[n]);
	return 0;
}
posted @   Cl41Mi5deeD  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示