• 斜率优化学习笔记


    斜率优化学习笔记

    (解释太麻烦,这里笼统的讲一下,具体看书,书上更详尽,所以无视这部分)

    斜率优化通常一般可以将(n^2)的DP优化到(O(n)) ,是一个很强的优化。

    形如:

    [f[i]=min{f[j]+(s[i]-s[j])^2}(i-dleq j) ]

    看似好像可以用单调队列的,但是无法把(i,j)分开的DP,大部分可以用斜率优化

    一般斜率优化具有单调性,可以用单调队列维护有效决策;

    单调性什么意思?有效决策什么意思?

    例如这个方程,把它变一下,(min)去掉,把关于(j)的都移到左边,关于(i)(i,j)的移到右边

    [f[j]+s[j]^2=2*s[i]*s[j]+f[i]-s[i]^2 ]

    (s[i]​)都是已知的,要在一堆({f[j],s[j]}​)中选出一个使得(f[i]​)最小;

    那么对于每一个({f[j],s[j]}) ,放在平面直角坐标系中设(f[j]+s[j]^2)(y)(s[j])(x) ,可以画出一条斜率为(2*s[i])的直线,那么这条线的截距就为(f[i]-s[i]^2)

    那么使得截距最小的({f[j],s[j]}) 就为所求。

    所以决策就是这些({f[j],s[j]}) ,而有效决策就是对求(f[i])有贡献的决策。

    这里(s[i])单调递增,就因为着斜率单调递增,每次新加入的决策也在最右边。

    然后结合书上讲的有些关于凹凸的东西,直线平移,什么的,看书。(滑稽)

    (ps: 加黑的都是斜率优化的套路)

    列出斜率方程后就基本上可以直接套板子了;


    任务安排2

    一开始想到暴力DP方程:(设(f[i][j])为前(j)个任务分为(i)批的最小费用,(sum)为各种前缀和)

    [f[i][j]=min{f[i-1][k]+(S*i+sumT[j])*(sumC[j]-sumC[k])} ]

    拆开也是可以斜率优化的,因为拆开后只会得到一项(j,k)无法分开的,这是可以套板子的,但是(i)是没有限制的,所以难以得出答案;

    那么考虑当每分一个组的时候后面的时间都会往后移(S) ,那么最后总费用就会增加(S*(sumC[n]-sumC[j])) ,那就好办了,设(f[i])为前(i)个任务的最小费用,就有状态转移方程:

    [f[i]=min{f[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[n]-sumC[i])} ]

    其实这里(f[i])的定义有误,应该为前(i)个任务对最后答案的最小贡献。(无视这句话也可以写)

    转化为斜率方程:

    [f[j]=(S+sumT[i])*sumC[j]+f[i]-sumT[i]*sumC[i]-S*sumC[n] ]

    这里斜率和加入点的横坐标是单调递增的,然后又可以开心的套板子了。


    任务安排3

    和上题不一样的地方就是列出的斜率方程,斜率没有单调性,但是有效决策的横坐标的加入还是单调递增的,那就二分查找最优的决策就可以了。

    #include<iostream>
    #include<cstdio>
    #define ll long long
    using namespace std;
    const int N=3e5+5;
    ll f[N],n,s;
    ll sumc[N],sumt[N];
    ll head=1,tail=1;
    ll q[N];
    ll read()
    {
        ll f=1;
        char ch;
        while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
        ll res=ch-'0';
        while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
        return res*f;
    }
    ll check(int i)
    {
        if(head==tail) return q[head];
        int l=head,r=tail;
        while(l<r)
        {
            int mid=(l+r)>>1;
            if(f[q[mid+1]]-f[q[mid]]<=(s+sumt[i])*(sumc[q[mid+1]]-sumc[q[mid]])) l=mid+1;
            else r=mid;
        }
        return q[r];
    }
    void solve(int i)
    {
        ll j=check(i);
        f[i]=f[j]-(s+sumt[i])*sumc[j]+sumt[i]*sumc[i]+sumc[n]*s;
        while(head<tail&&(f[i]-f[q[tail]])*(sumc[q[tail]]-sumc[q[tail-1]])<=(f[q[tail]]-f[q[tail-1]])*(sumc[i]-sumc[q[tail]])) tail--;
        q[++tail]=i;
    }
    int main()
    {
    #ifndef ONLINE_JUDGE
        freopen("task.in","r",stdin);
        freopen("task.out","w",stdout);
    #endif
        n=read(),s=read();
        for(int i=1;i<=n;i++)
        {
            ll t=read(),c=read();
            sumt[i]=sumt[i-1]+t;
            sumc[i]=sumc[i-1]+c;
        }
        for(int i=1;i<=n;i++) solve(i);
        printf("%lld
    ",f[n]);
        return 0;
    }
    

    Cats Transport

    这题有个巧妙的思路(巧妙的思路当然不是我想出来的啊)(a[i]=t[i]-sum_{j=1} ^i d[j]) ,这样就可以表示出如果饲养员要接到第(i)只猫就要在(a[i])之后出发,且猫(i)等待的时间为(t-a[i])

    那么这样就好办了,只要将(a[i])从小到大排序,求出前缀和(suma[i]),那要带走区间([l+1,r])的猫,这些猫要等待待的时间为(a[r]*(r-l)-(suma[r]-suma[l]))

    这样就有状态转移方程:

    [f[i][j]=min{f[i-1,k]+a[j]*(j-k)-(suma[j]-suma[k])} ]


    玩具装箱

    直接划分就好了,设(f[i])为前(i)个的最小价值,(Sum[i])为加上了填充物之后的长度的前缀和,即(Sum[i]=Sum[i-1]+c[i]+1) ,这样就有DP方程:

    [f[i]=min{f[j]+(Sum[i]-(Sum[j]+1)-L)^2} ]

    为了简化式子,再设(S[j]=Sum[j]+1) ,然后去掉(min)把这个式子变为斜率优化通常的亚子:

    [f[j]+S[j]^2+2*L*S[j]=2*Sum[i]*S[j]+f[i]-(Sum[i]-L)^2 ]

    然后以此建立坐标系,方程左边为(y)(S[j])(x) ,然后每条线斜率为(2*Sum[i])


    仓库建设

    为了把最后得到的斜率方程使得斜率和(x)都单调递增,我尝试了一堆表示方法(甚至一度怀疑是不是又要二分了),不过最后还是在我不懈的努力下列出来了(哭泣)

    由题意可得最后一个工厂一定要设置仓库,所以可以设(f[i])为前(i)个工厂的最小花费,且第(i)个工厂建设仓库,这样(f[n]) 就是最后的答案;

    下面就是我想了很久的东西了:(ps:(d[i])为题目中的(x[i])

    对于每个工厂,(d[i]*p[i])为每个工厂送到工厂1所需要的费用,那么设(sumf[i])为这费用的前缀和,然后再设(sump[i])(p[i])的前缀和,所以对于区间([l,r])的物品全部运到(r)所需要的费用为(d[r]*(sump[r]-sump[l])-(sumf[r]-sumf[l])) ,可以通过画图理解;

    所以有状态转移方程:

    [f[i]=min{f[j]+d[i]*(sump[i]-sump[j])-(sumf[i]-sumf[l])+c[i]} ]

    变成斜率优化的方程,把关于(j)的都移到左边,关于(i)(i,j)的移到右边:

    [f[j]+sumf[j]=d[i]*sump[j]+f[i]-d[i]*sump[i]+sumf[i]-c[i] ]

    斜率为(d[i])(x)(sump[j]) ,他们都单调递增,那就写完了,套板子就好了。

    #include<iostream>
    #include<cstdio>
    #define ll long long
    using namespace std;
    const int N=1e6+5;
    int n;
    ll sumf[N],sump[N],d[N],c[N],f[N];
    ll q[N],x[N],y[N];
    ll read()
    {
        ll f=1;
        char ch;
        while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
        ll res=ch-'0';
        while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
        return res*f;
    }
    int main()
    {
    #ifndef ONLINE_JUDGE
        freopen("publi.in","r",stdin);
        freopen("publi.out","w",stdout);
    #endif
        n=read();
        for(int i=1;i<=n;i++)
        {
            d[i]=read();
            ll p=read();
            c[i]=read();
            sumf[i]=sumf[i-1]+d[i]*p;
            sump[i]=sump[i-1]+p;
        }
        int head=1,tail=1;
        for(int i=1;i<=n;i++)
        {
            while(head<tail&&(y[head+1]-y[head])<=d[i]*(x[head+1]-x[head])) head++;
            f[i]=f[q[head]]+d[i]*(sump[i-1]-sump[q[head]])-(sumf[i-1]-sumf[q[head]])+c[i];
            while(head<tail&&(f[i]+sumf[i]-y[tail])*(x[tail]-x[tail-1])<=(y[tail]-y[tail-1])*(sump[i]-x[tail])) tail--;
            q[++tail]=i,y[tail]=f[i]+sumf[i],x[tail]=sump[i];
        }
        printf("%lld",f[n]);
        return 0;
    }
    

    特别行动队

    与前几题不也一样的地方就是斜率递减,但(x)还是递增;还是可以用单调队列,维护一个单调递减的斜率;在匹配斜率的时候只需要找到一个斜率比它小的就可以了;

    DP方程:

    [f[i]=max{f[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c} ]

    后面还是板子;


    打印文章

    板子;


    锯木厂选址

    和仓库建设类似,不过更简单,先(O(n))算出第一个锯木厂对于前(i)颗树的最优策略,然后求二个锯木厂最优策略就和仓库建设的思路一样了,最后再(O(n))枚举一下第二个锯木厂的位置,后面的都运到山底去,取个(min)就完事了;


    任务安排4

    以后要写的题。

    点的加入和斜率都不具有单调性,需要动态加点,那就需要平衡树维护,具体怎么操作,这里留个坑,等我写完了再补回来。

  • 相关阅读:
    linux(ubuntu) 安装 node.js
    正反向代理
    js正则表达式----replace
    webpack开发小总结
    稳定排序和非稳定排序
    树状数组 --- (离散化+树状数组、求逆序对)
    编程之美初赛第一场 题目3 : 活动中心
    巴什博弈 杭电 1847
    巴什博弈 杭电 1846
    约瑟夫环问题详解
  • 原文地址:https://www.cnblogs.com/Wednesday-zfz/p/13777569.html
Copyright © 2020-2023  润新知