• sss


    动态规划的单调性优化

    决策集合优化

    (mathrm{dp})的时候决策集合只扩大不减小,直接把最大值(/)最小值(/)累加和记下来就好了.

    例如:(mathrm{LCIS CH5101})(f_{i,j}=maxlimits_{0leq k<j,B_k<A_i}{f_{i-1,k}}+1).

    外层(i)不变,随着(j)增大,每次决策(k)最多增加一个,判断一下是否合法记下来即可.

    单调队列优化

    (mathrm{dp})的时候决策区间上下界都单调变化,可以直接记录区间和的值. 如果是最优化(mathrm{dp}),那就要用滑动窗口记录区间最值.

    写代码的时候,在循环开始的时候排除队首过期决策,然后取队首作为最优解,然后加入新决策. 有些边界麻烦的初值可以暴力算掉.

    多重背包怎么用单调队列优化?先写方程:(f[j]=maxlimits_{1leq kleq c_i}{f[j-k imes v_i]+k imes w_i}).

    观察:决策点每次位移(v_i),那就把(j)按照(v_i)的余数分类. 设(j=u+v_i imes p),于是:

    [f[u+v_i imes p]=maxlimits_{max{0,p-c_i}leq kleq p-1}{f[u+v_i imes k]+(p-k) imes w_i} ]

    这样的话可以把(i,u)当成定值,枚举(p),此时决策变量(k)的上下界单调变化,就可以用单调队列了.

    斜率优化

    多项式(v(i,j))含有(i,j)乘积项的(mathrm{1D/1D})动态规划,一般可以用单调队列维护下凸壳(/)上凸壳.

    (f_i=maxlimits_{0leq jleq i-1}{f_j+A(i)+B(j)+p(i)q(j)}),然后移项一下:

    [f_j+B(j)=-p(i)q(j)-A(i)+f_i ]

    这时候把所有决策点(j)看成平面上的点(P_j(-q(j),f_j+B(j))),那么现在你要用一条斜率为(p(i))的直线去切这些点,求最小截距.

    一般来说,这些点都是单调递增的,所以我们可以用单调队列维护下凸壳作为最优决策集. 如果(p(i))也是单调的,只要在队首操作最优决策即可,如果(p(i))不单调,那就二分找最优决策点.

    如果这些点不是单调递增的话,那就要用平衡树(/)(mathrm{cdq})分治维护凸壳. 不过用李超树求一次函数最值会更简单.

    注意事项有点多:

    (1.) 决策变量有取值范围,在推入队列的时候改为推入可取的决策变量

    (2.) (dp)数组有部分初值无法用斜率优化转移得到(决策变量被取值范围限制),暴力先转移初值

    (3.) 斜率会出现整数被(0)除,特判返回(+infty)(-infty)

    (4.) 计算斜率可能会出锅,(mathrm{slope})函数尽量公式化:数值大的下标为(y),数值小的下标为(x),计算时用(val(y)-val(x))

    (5.) 单调队列要注意:必须在队列内有至少两个元素才能删除队首或队尾

    (6.) 浮点数运算很容易出问题,计算斜率或比较大小时记得转为(mathrm{double})类型

    (7.) 精度可能会出问题,适当时计算斜率的除法要转为乘法

    (8.) 考虑单调队列内是否要存一个转移初值(如(0))

    (9.) (mathrm{dp})数组的初值:(+infty)(-infty)(0),是否要(mathrm{long long}),无穷要开够大

    (10.) 弹出队首不优元素和队尾不在下凸壳内元素比较斜率时,请将等号加上((<)尽量写成(leq)(>)尽量写成(geq))

    四边形不等式

    直接记结论即可:对于整数域上的二元函数(w(x,y)),若其满足:

    [aleq bleq cleq d,w(a,d)+w(b,c)geq w(a,c)+w(b,d) ]

    或者

    [a< b,w(a,b+1)+w(a+1,b)geq w(a,b+1)+w(b,a+1) ]

    则称(w)满足四边形不等式.

    对于(f_i=minlimits_{0leq j < i}{f_j+w(j,i)})(w(a,b))满足四边形不等式,则(f)的决策数组(p)单调不减.

    暴力优化的话直接从上一个决策点开始枚举即可,如果用队列维护决策点连续段可以做到(O(nlog_2 n)),需要用二分找分界点. 如果是二维的(mathrm{dp}),每次仅从上一维转移,可以采用分治写法,时间复杂度也是每层(O(nlog_2 n)),如果一维(mathrm{dp})强行套(mathrm{cdq})分治的话,时间复杂度两个(log).

    对于(f_{i,j}=minlimits_{ileq k<j}{f_{i,k+1}+f_{k+1,j}+w(i,j)}),或者(f_{i,j}=minlimits_{i-1leq kleq j-1}{f_{i-1,k}+w(k+1,j)})(f_{i,i}=w_{i,i}=0)(forall aleq bleq cleq d,w(a,d)geq w(b,c))(w)满足四边形不等式,则(f)的决策数组(p)有二维决策单调性:

    [forall i < j,p_{i,j-1}<p_{i,j}<p_{i+1,j} mathrm{or} p_{i-1,j}<p_{i,j}<p_{i,j+1} ]

    按照长度为阶段的区间(mathrm{dp}),直接在决策范围里面枚举时间复杂度就优化到(O(n^2)),序列分段型的(mathrm{dp})每段倒序更新也可以优化到(O(n^2)).

    习题

    BZOJ 4709 柠檬

    CF868F Yet Another Minimization Problem

    还没来得及做这题就被出在考试里了(...)

    首先列出方程:

    [f_{i,j}=min_{k<j}left{f_{i-1,k}+sum_{p=k+1}^jsum_{q=p+1}^j[a_p=a_q] ight} ]

    (w(l,r)=sum_{i=l}^rsum_{j=i+1}^r[a_i=a_j]),显然(w(i,i)=f_{i,i}=0),并且包含单调,现在我们要证明(w(l,r))满足四边形不等式. 要证:

    [w(l,r+1)+w(l+1,r)geq w(l,r)+w(l+1,r+1) \ Rightarrow w(l,r+1)-w(l,r)geq w(l+1,r+1)-w(l+1,r) ]

    (f(l)=w(l,r+1)-w(l,r)),展开:

    [f(l)=sum_{i=l}^{r+1}sum_{j=i+1}^{r+1}[a_i=a_j]-sum_{i=l}^rsum_{j=i+1}^{r}[a_i=a_j] \ =sum_{i=l}^{r}left(sum_{j=i+1}^{r+1}[a_i=a_j]-sum_{j=i+1}^r[a_i=a_j] ight)=sum_{i=l}^r[a_{r+1}=a_i] ]

    显然有(f(l)geq f(l+1)),所以(w)满足四边形不等式,原动态规划具有决策单调性.

    现在,我们显然可以通过桶来(O(n))计算一次转移的贡献,但是这样太慢了,似乎也没有什么好的办法.

    一个想法是用类似于莫队的指针移动的方法优化,不过直接利用决策单调性倒序转移,指针移动的量还是(O(n)). 不妨考虑决策单调性的分治算法.

    我们可以让区间的指针一开始移动到分治决策区间的左右端点,那么对于分治函数(mathrm{d}(l,r,L,R)),其指针移动的量就是(O(R-L))的,递归的时候把它移到对应的位置,移动的量也是(O(R-L)),所以根据分治的复杂度分析,由((l,r))可以控制分治层数不超过(O(log_2 n))层,且分治树上相同深度节点的时间贡献和为(Oleft(sum(R-L) ight)=O(n)),那么总复杂度是不变的,也就是(O(knlog_2 n)),可以通过.

    #include <bits/stdc++.h>
    using namespace std;
    #define Rep(i,a,b) for (int i = a, _ = b; i <= _; i++)
    #define iRep(i,a,b) for (int i = a, _ = b; i >= _; i--)
    typedef long long ll;
    const int N = 1e5 + 20, K = 22;
    int n,k,a[N],buc[N],nl,nr; ll Cost,f[K][N];
    inline void Insert(int x) { Cost += buc[a[x]], ++buc[a[x]]; }
    inline void Remove(int x) { --buc[a[x]], Cost -= buc[a[x]]; }
    inline ll w(int tl,int tr) {
        while ( nl > tl ) Insert(--nl); while ( nr < tr ) Insert(++nr);
        while ( nl < tl ) Remove(nl++); while ( nr > tr ) Remove(nr--);
        return Cost;
    }
    inline void Divide(int p,int l,int r,int L,int R)
    {
        if ( l > r || L > R ) return void();
        if ( l == r ) { Rep( i, L, min(R,l-1) ) f[p][l] = min( f[p][l], f[p-1][i] + w(i+1,l) ); return void(); }
        if ( L == R ) { Rep( i, max(l,L+1), r ) f[p][i] = min( f[p][i], f[p-1][L] + w(L+1,i) ); return void(); }
        int mid = l + r >> 1, M = L;
        for (int i = L; i <= min(mid-1,R); i++)
            if ( f[p][mid] > f[p-1][i] + w(i+1,mid) )
                f[p][mid] = f[p-1][i] + w(i+1,mid), M = i;
        return Divide(p,l,mid-1,L,M), Divide(p,mid+1,r,M,R);
    }
    int main(void)
    {
        scanf( "%d%d", &n, &k );
        Rep( i, 1, n ) scanf( "%d", &a[i] );
        memset( f, 0x3f, sizeof f ), f[0][0] = 0;
        nl = nr = 1, Cost = 0, buc[a[1]]++;
        for (int i = 1; i <= k; i++) Divide( i, 1, n, 0, n-1 );
        return printf( "%lld
    ", f[k][n] ) * 0;
    }
    
    

    CF321E Ciel and Gondolas

    HDU3480 Division

    首先注意到划分的集合肯定是在有序序列上划分连续段,那么就可以设(f_{i,j})表示前(j)个数字划分了(i)段的最小权值和.于是就有:

    [f_{i,j}=min_{kin[0,j-1]}{f_{i-1,k}+(a_j-a_{k+1})^2} ]

    看到这里,你已经可以无脑斜率优化过掉这题了. 不过我们要讨论一下另一个东西:决策单调性.

    (w(i,j)=(a_j-a_{i})^2),那么要证明(w(i,j+1)+w(i+1,j)geq w(i,j)+w(i+1,j+1)),只要证明:(w(i+1,j)-w(i,j)geq w(i+1,j+1)-w(i,j+1)).

    (f(j)=w(i+1,j)-w(i,j)),显然有:

    [f(j)=(a_j-a_{i+1})^2-(a_j-a_{i})^2=a_{i+1}^2-a_{i}^2-2a_j(a_{i+1}-a_{i}) ]

    显然(a_{i+1}-a_{i})为正,则(f(j))(j)的增加而递减,于是(w(i+1,j)-w(i,j)geq w(i+1,j+1)-w(i,j+1))成立,(w)满足四边形不等式,且(w(i,j))包含单调,(f_{i,i}=w_{i,i}=0),所以原动态规划具有决策单调性.

    这时候你可以直接写分段分治,代码十分简单,时间复杂度(O(mnlog_2n)),但是这样显然没有充分利用决策单调性带来的优势,我们可以倒序递推,这样时间复杂度就是(O(nm)),比斜率优化好写,细节也更少.

    #include <bits/stdc++.h>
    using namespace std;
    #define Rep(i,a,b) for (int i = a, _ = b; i <= _; i++)
    const int N = 10020, M = 5020;
    int n,m,a[N],f[M][N],p[M][N];
    int main(void)
    {
        int T,t; scanf( "%d", &T );
        while ( ++t <= T ) {
            scanf( "%d%d", &n, &m );
            Rep( i, 1, n ) scanf( "%d", &a[i] );
            sort( a + 1, a + n + 1 );
            memset( f, 0x3f, sizeof f ), f[0][0] = f[1][0] = 0;
            for (int i = 1; i <= n; i++) f[1][i] = ( a[i] - a[1] ) * ( a[i] - a[1] ), p[1][i] = 0;
            for (int i = 2; i <= m; i++) {
                p[i][n+1] = n - 1;
                for (int j = n; j >= i; j--)
                    for (int k = p[i-1][j]; k <= p[i][j+1]; k++)
                        if ( f[i][j] > f[i-1][k] + ( a[j] - a[k+1] ) * ( a[j] - a[k+1] ) )
                            f[i][j] = f[i-1][k] + ( a[j] - a[k+1] ) * ( a[j] - a[k+1] ), p[i][j] = k;
            }
            printf( "Case %d: %d
    ", t, f[m][n] );
        }
        return 0;
    }
    
    

    任务安排4

    首先写出方程:

    [f_i=min_{jin[0,i-1]}left{f_{j}+left(sum_{k=1}^it_k ight)left(sum_{k=1}^ic_k-sum_{k=1}^jc_k ight)+sleft(sum_{k=1}^nc_k-sum_{k=1}^jc_k ight) ight} ]

    用前缀和整理一下就是:

    [f_i=min_{jin[0,i-1]}{f_j+T_i(C_i-C_j)+s(C_n-C_j)} ]

    如果要斜率优化,就整理成:

    [underset{y}{underline{f_j-sC_j}}=underset{k}{underline{T_i}}underset{x}{underline{C_j}}-underset{b}{underline{T_iC_i-sC_n+f_i}} ]

    然而(P_j(C_j,f_j-sC_j))没有任何单调性,不能直接用单调队列维护凸壳.

    (mathrm{cdq})分治太它喵难写了,直接上李超树,不需要任何单调性.

    注意线段树要动态开点,值域要开够,对于决策点(j=0)的情况线段树考虑不到,要单独转移. 最小值李超树,线段下放细节要考虑清楚,查询的时候注意在每一个遍历到的线段上都更新一遍答案.

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N = 3e5 + 20, INF = 1e9;
    struct Line { ll k,b; };
    struct Node { int ls,rs; Line L; };
    struct LiChaoTree {
        Node ver[N*50]; int tot,rt;
        #define ls(p) ver[p].ls
        #define rs(p) ver[p].rs
        #define L(p) ver[p].L
        #define mid ( l + r >> 1 )
        #define Ls ls(p), l, mid
        #define Rs rs(p), mid + 1, r
        inline ll Calc(int x,Line l) { return 1LL * l.k * x + l.b; }
        inline double Inter(Line a,Line b) { return ( b.b - a.b ) / ( a.k - b.k ); }
        inline void Modify(int &p,int l,int r,int ml,int mr,Line x) {
            if (!p) p = ++tot; if ( l > r || ml > mr || mr < l || ml > r ) return void();
            if ( l < ml || r > mr ) return Modify(Ls,ml,mr,x), Modify(Rs,ml,mr,x);
            if ( !L(p).k ) return void( L(p) = x ); ll vl1,vl2,vr1,vr2;
            vl1 = Calc(l,L(p)), vr1 = Calc(r,L(p)), vl2 = Calc(l,x), vr2 = Calc(r,x);
            if ( vl1 <= vl2 && vr1 <= vr2 ) return void();
            if ( vl1 > vl2 && vr1 > vr2 ) return void( L(p) = x );
            if ( vl1 <= vl2 ) return Inter(L(p),x) > mid ?
                Modify(Rs,ml,mr,x) : ( swap(L(p),x) , Modify(Ls,ml,mr,x) );
            if ( vl1 > vl2 ) return Inter(L(p),x) > mid ?
                ( swap(L(p),x) , Modify(Rs,ml,mr,x) ) : Modify(Ls,ml,mr,x);
        }
        inline ll Query(int p,int l,int r,int x) {
            if ( l == r ) return Calc(x,L(p)); if (!p) return 1LL * 1000 * INF;
            return min( Calc(x,L(p)), x <= mid ? Query(Ls,x) : Query(Rs,x) );
        }
    } T;
    int n,s,t[N],c[N]; ll f[N];
    int main(void)
    {
        scanf( "%d%d", &n, &s );
        for (int i = 1; i <= n; i++)
            scanf( "%d%d", &t[i], &c[i] ), t[i] += t[i-1], c[i] += c[i-1];
        f[1] = 1LL * t[1] * c[1] + 1LL * c[n] * s;
        T.Modify( T.rt, -INF, INF, -INF, INF, { -c[1], f[1] - 1LL * s * c[1] } );
        for (int i = 2; i <= n; i++) {
            f[i] = T.Query( T.rt, -INF, INF, t[i] ) + 1LL * t[i] * c[i] + 1LL * s * c[n];
            f[i] = min( f[i], 1LL * t[i] * c[i] + 1LL * c[n] * s );
            T.Modify( T.rt, -INF, INF, -INF, INF, { -c[i], f[i] - 1LL * s * c[i] } );
            // printf( "f[%d] = %lld
    ", i , f[i] );
        }
        return printf( "%lld
    ", f[n] ) * 0;
    }
    
    
  • 相关阅读:
    sql server 扩展存储过程
    SQL SERVER 数据导出JSON
    Jquery AJAX ASP.NET IIS 跨域 超简单解决办法
    切换 NPM 镜像源
    HTML 简介
    mongoose 安装及配置
    Windows下MongoDB的安装过程及基本配置
    Node.js中npm常用命令大全
    --nodejs详细安装步骤
    Go语言为何说它优雅?-- Golang中的几个常用初始化设计
  • 原文地址:https://www.cnblogs.com/Parsnip/p/13373161.html
Copyright © 2020-2023  润新知