关于斜率优化de笔记

乍一看这个名字就非常的恐怖,实际上也很恐怖

但是还是被迫学习了这一优化方法

1.作用

它的主要作用就是帮本来就不是一般的快的DP剪枝,使其变得更是不一般的快

用这种方法,我们可以筛除掉大量的数据。

在最好的情况下,施展此魔法甚至可以让O(n)变为O(1)

2.条件!

听到此算法的名字,就会想到它与斜率有关。

——我们需要数形结合的思想,把它画在平面直角坐标系里头。

因此,此算法的前提就是一定要能把状态转移方程化成y=kx+b的形式

3.正文开始

一本通的1608为例。

很明显这是一道DP题,一般的做法是写一个区间dp,但是很不幸数据过大,两重循环的区间DP只能拿20分。这时候就需要斜率优化。

原来的状态转移方程:

f[i]=min(f[i],f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]))

去掉min,再化简一下,得到新的f的计算方法为:

f[i]=f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]);
=>f[j]= st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j])-f[i]
=>f[j]= sc[j]*(s+st[i])-st[i]*sc[i]+f[i]-s*sc[n]

把j作为x,则可以把整个式子视为一个一次函数(y;f[j],k:s+st[i],b: -st[i]*sc[i]+f[i]-s*sc[n]).

以f为y轴,j为x轴,把点们画在平面直角坐标系中。当斜率确定时,情况是这样的:

       那么可以看出,此时影响f大小的就是截距(b)。很明显3是最优解,截距也最小。

      稍微理解一下,当一条斜率为k的直线从b等于无限小处开始向上平移(b增大),最先碰到的是点3。

      让我们先来多画几个点:

        可以发现,无论斜率是多少,点1、2、4都不能成为“被最先碰到的点”,也就是无论如何都不可能成为最小值。所以这些点可以被去掉。剩下的点就成为决策点。(最优解只可能从决策点中被选出

把决策点们连起来(棕线):

它们形成了一个下凸包类似于a>0的二次函数

so 可以得出,斜率要递增才能成为决策点(看图时注意:不仅仅是斜度增加,还有k由负变正!)

 3.例题

        1)ybt 1607

                题目传送门

                首先丢掉1608,让我们来看简单一点的1607

                这道题我们可以用单调队列+斜率优化。虽然由于数据太水,朴素方法也能过

                具体的解释在程序中有,就不多讲了

#include <bits/stdc++.h>
using namespace std;
long long n,s,t,c,f[300010],st[300010],sc[300010];
int l,r,q[300010];
//deque<int>q;
int main(){
	memset(f,0x3f,sizeof(f));
	f[0]=0;//f[0]不赋0,f数组就会无限大 
	cin>>n>>s;
	for(int i=1;i<=n;i++)
	{
		cin>>t>>c;
		st[i]=st[i-1]+t;
		sc[i]=sc[i-1]+c; 
	}
	l=r=1;
	for(int i=1;i<=n;i++)
		{
			//斜率即是时间,最简单的一种方法:不分割,一次做完,即t=s+st[i],以此为基础,
			//因为斜率被维护单调递增(斜率越大截距越小),需要找斜率大于s+st[i]的第一个点 
			while(l<r&&f[q[l+1]]-f[q[l]]<=(s+st[i])*(sc[q[l+1]]-sc[q[l]])) l++;
			//等同于(f[q[l+1]]-f[q[l]])/(sc[l]-sc[l-1])<= s+st[i] q[l]代替j 
			f[i]=f[q[l]]-(s+st[i])*sc[q[l]]+st[i]*sc[i]+s*sc[n];
			/*
			f[i]=min(f[i],f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]));
			f[j]=(s+st[i])*sc[j]+f[i]-sc[i]*st[i]-s*sc[n]
			=>f[i]=f[j]-((s+st[i])*sc[j]-sc[i]*st[i]-s*sc[n])
			如果有可能成为最终答案,这个点一定是在图像上凸出来的(凸包) 
			以f作y轴,sc作x轴,即(f[i1]-f[i2])/(sc[i1]-sc[i2])<(f[i2]-f[i3])/(sc[i2]-sc[i3]) 
			=>维护凸包*/ while(l<r&&(f[q[r]]-f[q[r-1]])*(sc[i]-sc[q[r]])>=(f[i]-f[q[r]])*(sc[q[r]]-sc[q[r-1]])) r--;
			r++;
			q[r]=i;
		}
	cout<<f[n]<<endl;
	return 0;
}

        2)1608

                题目传送门

                然后回到1608

                其实两题连题目都一模一样,按时间复杂度来说理论上也能过,但是为什么WA了且只有

60分呢?

                这就是原因:

                这意味着Ti可能为负,所以单调队列会出错把一些决策点剪掉,虽然做一件事用时是负数这很不合理

                因此,这里需要用二分。不开longlong见祖宗

#include <bits/stdc++.h>
using namespace std;
long long n,s,st[300001],sc[300001],l,r,j;
long long f[300001],q[300001];
long long find(long long l,long long r,long long t){//二分查找最符合的点 
	while(l<r){
		int mid=(l+r)/2;
		if((f[q[mid]]-f[q[mid+1]])>t*(sc[q[mid]]-sc[q[mid+1]]))l=mid+1;
		else r=mid;
	}
	return l;
}
int main(){
	scanf("%lld%lld",&n,&s);
	for(long long i=1;i<=n;i++)
		scanf("%lld%lld",&st[i],&sc[i]),st[i]=st[i-1]+st[i],sc[i]=sc[i]+sc[i-1]; 
	for(long long i=1;i<=n;i++){
		j=q[find(l,r,st[i]+s)];
		f[i]=f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]);//化简后的状态转移方程 
		while(l<r&&(f[q[r-1]]-f[q[r]])*(sc[q[r]]-sc[i])>=(f[q[r]]-f[i])*(sc[q[r-1]]-sc[q[r]])) r--;
		//维护凸包 
		r++; 
		q[r]=i;
	}
	
	printf("%lld\n",f[n]);
	return 0;
}

 完结撒花~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值