斜率优化DP(学习笔记)

膜拜yyb大佬的总结

因为上面yyb大佬把斜率优化的板子讲得很清楚,所以我就不再赘述了.就来看几套模板题吧.

蓝书和一本通提高篇上都是以"任务安排"这道题为例题层层递进讲解斜率优化的.

洛咕上的弱化版,不用斜率优化也能过

POJ上的普通版:斜率优化模板

BZOJ上的加强版:斜率优化+二分

题意: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\n",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\n",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\n",f[n]);
    return 0;
}

个人对斜率优化的一些理解:首先前置知识---单调队列一定要掌握.其次因为毕竟是DP的优化方法之一,拿到题目,我们要先设好状态,列出转移方程(斜率优化的题目,\(O(n^2)\)的转移方程一般都很容易得到).

得到转移方程之后考虑整理这个式子,一般都要用到前缀和,然后一个普遍的套路就是yyb大佬也提到了的"设k<j且j比k更优",然后就能得到一个关于j和k与一个常数的不等式,这时就可以进行斜率优化了.

然后一些细节问题需要自己领悟,比如维护的单调队列是递增还是递减?前缀和是否要开long long?维护的时候肯定要比较大小,大多数人一般都是写个函数用double,可是我个人在写下面"Print Article"这道题时用double过不去,换成long long就过了,像这种比较玄学的东西,就拼人品了,大不了挨个试一遍.

[HNOI2008]玩具装箱TOY

[APIO2010]特别行动队

Print Article

[CEOI2004]锯木厂选址

[ZJOI2007]仓库建设

猜你喜欢

转载自www.cnblogs.com/PPXppx/p/11007448.html
今日推荐