因为上面yyb大佬把斜率优化的板子讲得很清楚,所以我就不再赘述了.就来看几套模板题吧.
蓝书和一本通提高篇上都是以"任务安排"这道题为例题层层递进讲解斜率优化的.
题意:N个任务,每个任务有一个完成所需时间(t_i),试将这N个任务分成若干批,在每批任务开始前,机器需要启动时间S,这批任务完成所需时间为各个任务需要时间的总和(同一批任务将在同一时刻完成).每个任务的费用是它的完成时刻乘上一个费用系数(c_i).求最小总费用.
设(f[i])表示把前i批任务分成若干批的最小费用.运用"费用提前计算"的思想,有如下转移方程:
(f[i]=min_{0<=j<i}f[j]+sumt[i]*(sumc[i]-sumc[j])+S*(sumc[N]-sumc[j]))
只能过洛咕上的(N^2)做法:
const int N=5005;
int sumt[N],sumc[N],f[N];
int main(){
int n=read(),S=read();
for(int i=1;i<=n;i++){
int t=read(),c=read();
sumt[i]=sumt[i-1]+t;
sumc[i]=sumc[i-1]+c;
}
memset(f,0x3f,sizeof(f));f[0]=0;
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
f[i]=min(f[i],f[j]+sumt[i]*(sumc[i]-sumc[j])+S*(sumc[n]-sumc[j]));
printf("%d
",f[n]);
return 0;
}
POJ上(N^2)做法不能过了,考虑对转移式优化.(f[i]=min_{0<=j<i}f[j]+sumt[i]*(sumc[i]-sumc[j])+S*(sumc[N]-sumc[j]))
设(k<j),且j比k更优,即,
(f[j]+sumt[i]*(sumc[i]-sumc[j])+S*(sumc[N]-sumc[j])<f[k]+sumt[i]*(sumc[i]-sumc[k])+S*(sumc[N]-sumc[k]))
乱搞一下这个式子得到,
(frac {f[j]-f[k]}{sumc[j]-sumc[k]}<S+sumt[i])
能过POJ的做法(O(N)):
const int N=300005;
int sumt[N],sumc[N],q[N],f[N];
int main(){
int n=read(),S=read();
for(int i=1;i<=n;i++){
int t=read(),c=read();
sumt[i]=sumt[i-1]+t;
sumc[i]=sumc[i-1]+c;
}
memset(f,0x3f,sizeof(f));f[0]=0;
int l=1,r=1;q[1]=0;
for(int i=1;i<=n;i++){
while(l<r&&(f[q[l+1]]-f[q[l]])<=(S+sumt[i])*(sumc[q[l+1]]-sumc[q[l]]))l++;
f[i]=f[q[l]]+(sumt[i]*sumc[i]+S*sumc[n])-(S+sumt[i])*sumc[q[l]];
while(l<r&&(f[q[r]]-f[q[r-1]])*(sumc[i]-sumc[q[r]])>=(f[i]-f[q[r]])*(sumc[q[r]]-sumc[q[r-1]]))r--;
q[++r]=i;
}
printf("%d
",f[n]);
return 0;
}
题目加强:任务的执行时间(t_i)可能是个负数,这说明(sumt[i])不再保证单调性,所以我们必须要维护整个队列,队头也不一定最优,每次转移时需要二分查找,找到一个位置j,j左侧线段的斜率比(S+sumt[i])小,j右侧线段的斜率比(S+sumt[i])大
能过BZOJ的做法:
const int N=300005;
LL sumt[N],sumc[N],q[N],f[N];
inline int erfen(int i,int j,int l,int r){
if(l==r)return q[l];
while(l<r){
int mid=(l+r)>>1;
if(f[q[mid+1]]-f[q[mid]]<=j*(sumc[q[mid+1]]-sumc[q[mid]]))l=mid+1;
else r=mid;
}
return q[l];
}
int main(){
int n=read(),S=read();
for(int i=1;i<=n;i++){
int t=read(),c=read();
sumt[i]=sumt[i-1]+t;
sumc[i]=sumc[i-1]+c;
}
int l=1,r=1;
for(int i=1;i<=n;i++){
int j=erfen(i,S+sumt[i],l,r);
f[i]=f[j]+(sumt[i]*sumc[i]+S*sumc[n])-(S+sumt[i])*sumc[j];
while(l<r&&(f[q[r]]-f[q[r-1]])*(sumc[i]-sumc[q[r]])>=(f[i]-f[q[r]])*(sumc[q[r]]-sumc[q[r-1]]))r--;
q[++r]=i;
}
printf("%lld
",f[n]);
return 0;
}
个人对斜率优化的一些理解:首先前置知识---单调队列一定要掌握.其次因为毕竟是DP的优化方法之一,拿到题目,我们要先设好状态,列出转移方程(斜率优化的题目,(O(n^2))的转移方程一般都很容易得到).
得到转移方程之后考虑整理这个式子,一般都要用到前缀和,然后一个普遍的套路就是yyb大佬也提到了的"设k<j且j比k更优",然后就能得到一个关于j和k与一个常数的不等式,这时就可以进行斜率优化了.
然后一些细节问题需要自己领悟,比如维护的单调队列是递增还是递减?前缀和是否要开long long?维护的时候肯定要比较大小,大多数人一般都是写个函数用double,可是我个人在写下面"Print Article"这道题时用double过不去,换成long long就过了,像这种比较玄学的东西,就拼人品了,大不了挨个试一遍.