乍一看这个名字就非常的恐怖,实际上也很恐怖
但是还是被迫学习了这一优化方法
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;
}
完结撒花~