『动态规划的单调性优化』

动态规划的单调性优化

决策集合优化

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

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

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

单调队列优化

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

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

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

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

\[f[u+v_i\times p]=\max\limits_{\max\{0,p-c_i\}\leq k\leq p-1}\{f[u+v_i\times k]+(p-k)\times w_i\} \]

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

斜率优化

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

\(f_i=\max\limits_{0\leq j\leq 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)\),若其满足:

\[a\leq b\leq c\leq 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=\min\limits_{0\leq j < i}\{f_j+w(j,i)\}\)\(w(a,b)\)满足四边形不等式,则\(f\)的决策数组\(p\)单调不减.

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

对于\(f_{i,j}=\min\limits_{i\leq k<j}\{f_{i,k+1}+f_{k+1,j}+w(i,j)\}\),或者\(f_{i,j}=\min\limits_{i-1\leq k\leq j-1}\{f_{i-1,k}+w(k+1,j)\}\)\(f_{i,i}=w_{i,i}=0\)\(\forall a\leq b\leq c\leq 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}^j\sum_{q=p+1}^j[a_p=a_q]\right\} \]

\(w(l,r)=\sum_{i=l}^r\sum_{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}^r\sum_{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] \right)=\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)\)层,且分治树上相同深度节点的时间贡献和为\(O\left(\sum(R-L)\right)=O(n)\),那么总复杂度是不变的,也就是\(O(kn\log_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\n", f[k][n] ) * 0;
}

CF321E Ciel and Gondolas

HDU3480 Division

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

\[f_{i,j}=\min_{k\in[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(mn\log_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\n", t, f[m][n] );
    }
    return 0;
}

任务安排4

首先写出方程:

\[f_i=\min_{j\in[0,i-1]}\left\{f_{j}+\left(\sum_{k=1}^it_k\right)\left(\sum_{k=1}^ic_k-\sum_{k=1}^jc_k\right)+s\left(\sum_{k=1}^nc_k-\sum_{k=1}^jc_k\right)\right\} \]

用前缀和整理一下就是:

\[f_i=\min_{j\in[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\n", i , f[i] );
    }
    return printf( "%lld\n", f[n] ) * 0;
}

posted @ 2020-07-24 17:27  Parsnip  阅读(410)  评论(0编辑  收藏  举报