斜率优化dp

斜率优化

例题1:洛谷P2365 任务安排

我们先使用前缀和的方式,将 t [ i ] t[i] t[i] c [ i ] c[i] c[i]​​预处理, f [ i ] f[i] f[i]表示前面 i i i个整好之后的花费,然后得到方程式:
f [ i ] = m i n j = 0 i − 1 { f [ j ] + t [ i ] ∗ ( c [ i ] − c [ j ] ) + s ∗ ( c [ n ] − c [ j ] ) } f[i]=min_{j=0}^{i-1}\{f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]) \} f[i]=minj=0i1{f[j]+t[i](c[i]c[j])+s(c[n]c[j])}
其中 s ∗ ( c [ n ] − c [ j ] ) s*(c[n]-c[j]) s(c[n]c[j])​是费用提前计算的思想,在这里断一下的话,后面都会加上这么多,现在把 m i n min min去掉
f [ i ] = f [ j ] + t [ i ] ∗ ( c [ i ] − c [ j ] ) + s ∗ ( c [ n ] − c [ j ] ) f[i]=f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]) f[i]=f[j]+t[i](c[i]c[j])+s(c[n]c[j])

f [ j ] = f [ i ] + t [ i ] ∗ ( c [ j ] − c [ i ] ) + s ∗ ( c [ j ] − c [ n ] ) f[j]=f[i]+t[i]*(c[j]-c[i])+s*(c[j]-c[n]) f[j]=f[i]+t[i](c[j]c[i])+s(c[j]c[n])

f [ j ] ‾ y = ( t [ i ] + s ) ‾ k ∗ c [ j ] ‾ x + ( f [ i ] − s ∗ c [ n ] − t [ i ] ∗ c [ i ] ) ‾ b \underline{f[j]}_y=\underline{(t[i]+s)}_k*\underline{c[j]}_x+\underline{(f[i]-s*c[n]-t[i]*c[i])}_b f[j]y=(t[i]+s)kc[j]x+(f[i]sc[n]t[i]c[i])b

整个就可以看成 y = k ∗ x + b y=k*x+b y=kx+b​​​,其中 k k k​​​是不变的常数,其中 b b b​​​越小, f [ i ] f[i] f[i]​​​​越小,就越好,所以现在就去用 t [ i ] + s t[i]+s t[i]+s​去照着图整就完事了,也就是去找第一个​斜率比 t [ i ] + s t[i]+s t[i]+s大的那个点,也就是下图蓝色箭头所指的那个

图一.png

因为我们要 b b b越小越好,所以维护一个下凸包,如果是要 b b b​​越大越好,就维护一个上凸包,因为 t [ i ] + s t[i]+s t[i]+s是单调的,斜率只会越来越大,所以之前匹配的点,也就是斜率小于 t [ i ] + s t[i]+s t[i]+s的就可以直接弹掉

  • attention 1: 计算斜率的时候可能会遇到分母为 0 0 0的情况,小心哦
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 10000
using namespace std;
int n,s,t[maxn],c[maxn],f[maxn],q[maxn],l,r;
double slope(int x,int y) {return 1.0*(f[y]-f[x])/(c[y]-c[x]==0?(1e-10):(c[y]-c[x]));} 
signed main(){
	scanf("%d %d",&n,&s);
	for(int i=1;i<=n;i++){
		scanf("%d %d",&t[i],&c[i]);
		t[i]+=t[i-1]; c[i]+=c[i-1];
	}
	memset(f,127,sizeof f); f[0]=0;
	for(int i=1;i<=n;i++){
		while(r>l && slope(q[l],q[l+1])<(double)(t[i]+s)) l++;
		f[i]=f[q[l]]+t[i]*(c[i]-c[q[l]])+s*(c[n]-c[q[l]]);
		while(r>l && slope(q[r-1],q[r])>slope(q[r],i)) r--;
		q[++r]=i;
	}
	printf("%d\n",f[n]);
	return 0;
}

例题2:洛谷P5017 摆渡车

依然利用前缀和,这道题和上道题同处一辙啊, d p [ i ] dp[i] dp[i]​​​​依然表示前面 i i i​​​​个的花费,但是有一点不一样的是,同一组的同学等的时间不一样,但是这个也可以用前缀和的形式整出来,如果我们以时间轴来看,就是下面的这个样子,要比以 n n n​​​​来看要好得多,所以我们用两个数组 c n t [ i ] cnt[i] cnt[i]​​​​表示 i i i​​​​点有几个同学, s u m [ i ] sum[i] sum[i]​​​​​​表示当前这个时间戳的人的时间和,然后现在把他们两个数组以前缀和的形式弄出来,再然后,我们再来思考如何将中间那点人的等待时间表示出来,以中间那段来说,就是 7 , 9 , 10 7,9,10 7,9,10​​​​​的人到 10 10 10​​​​的时间,也就是 i ∗ ( c n t [ i ] − c n t [ j ] ) − ( s u m [ i ] − s u m [ j ] ) i*(cnt[i]-cnt[j]) -(sum[i]-sum[j]) i(cnt[i]cnt[j])(sum[i]sum[j])​​​​​​

图二.png
d p [ i ] = m i n j = 0 i − 1 { d p [ j ] + i ∗ ( c n t [ i ] − c n t [ j ] ) − ( s u m [ i ] − s u m [ j ] ) } dp[i]=min_{j=0}^{i-1}\{dp[j]+i*(cnt[i]-cnt[j]) -(sum[i]-sum[j]) \} dp[i]=minj=0i1{dp[j]+i(cnt[i]cnt[j])(sum[i]sum[j])}

d p [ i ] = d p [ j ] + i ∗ c n t [ i ] − i ∗ c n t [ j ] − s u m [ i ] + s u m [ j ] dp[i]=dp[j]+i*cnt[i]-i*cnt[j]-sum[i]+sum[j] dp[i]=dp[j]+icnt[i]icnt[j]sum[i]+sum[j]

( d p [ j ] + s u m [ j ] ) ‾ y = i ‾ k ∗ c n t [ j ] ‾ x + ( d p [ i ] − i ∗ c n t [ i ] + s u m [ i ] ) ‾ b \underline{(dp[j]+sum[j])}_y=\underline{i}_k*\underline{cnt[j]}_x+\underline{(dp[i]-i*cnt[i]+sum[i])}_b (dp[j]+sum[j])y=ikcnt[j]x+(dp[i]icnt[i]+sum[i])b

我们是要 d p [ i ] dp[i] dp[i]​越小越好,所以就是要 b b b越小越好,所以是维护下凸包

  • attention 1: s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) slope(q[tail-1],q[tail]) slope(q[tail1],q[tail])不能写成 s l o p e ( q [ t a i l ] q , [ t a i l − 1 ] ) slope(q[tail]q,[tail-1]) slope(q[tail]q,[tail1]),假如分母为 0 0 0的话,就会出现什么情况, s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) slope(q[tail-1],q[tail]) slope(q[tail1],q[tail])是大于 0 0 0的, s l o p e ( q [ t a i l ] q , [ t a i l − 1 ] ) slope(q[tail]q,[tail-1]) slope(q[tail]q,[tail1])是小于 0 0 0​的.
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 4000000
using namespace std;
int n,m,cnt[maxn],sum[maxn],dp[maxn],t,tail,head=1,q[maxn],ans=2147483647;
double slope(int x,int y) {return (double)(sum[y]+dp[y]-sum[x]-dp[x])*1.0/((cnt[y]==cnt[x]?1e-7:cnt[y]-cnt[x]));}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1,temp;i<=n;i++){
		 scanf("%d",&temp); t=max(t,temp);
		 cnt[temp]++; sum[temp]+=temp;
	}
	for(int i=1;i<=t+m;i++) cnt[i]+=cnt[i-1],sum[i]+=sum[i-1];
	memset(dp,127,sizeof dp); dp[0]=0;
	for(int i=1;i<=t+m;i++){
		while(tail>head && slope(q[tail-1],q[tail])>slope(q[tail],i-m)) tail--;
		if(i-m>=0) q[++tail]=i-m;
		while(tail>head && slope(q[head],q[head+1])<=(double)i) head++;
		dp[i]=cnt[i]*i-sum[i];
		if(i-m>=0) dp[i]=min(dp[q[head]]+i*(cnt[i]-cnt[q[head]])-sum[i]+sum[q[head]],dp[i]);
	}
	for(int i=t;i<=t+m;i++) ans=min(ans,dp[i]);
	printf("%d\n",ans);
	return 0;
}

总结
  • d p dp dp转移方程先写出来,然后再进行拆分组合,核心就是找到只有 j j j的,其他的都没啥关系,和 i i i有关系的一般都是常数或者定值,就没关系,随便放.
  • 斜率计算的小细节要注意:分母不为 0 0 0​,除的时候注意顺序
  • 一些技巧经常和这个一起用,如前缀和,费用提前计算之类的

因为是学习笔记,有什么问题还请指正,如果有哪里写漏了也欢迎指出.
点赞+关注😍

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

d3ac

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值