Processing math: 100%

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

Parsnip·2020-07-24 17:27·437 次阅读

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

动态规划的单调性优化#

决策集合优化#

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

例如:LCIS CH5101fi,j=max0k<j,Bk<Ai{fi1,k}+1.

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

单调队列优化#

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

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

多重背包怎么用单调队列优化?先写方程:f[j]=max1kci{f[jk×vi]+k×wi}.

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

f[u+vi×p]=maxmax{0,pci}kp1{f[u+vi×k]+(pk)×wi}

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

斜率优化#

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

fi=max0ji1{fj+A(i)+B(j)+p(i)q(j)},然后移项一下:

fj+B(j)=p(i)q(j)A(i)+fi

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

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

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

注意事项有点多:

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

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

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

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

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

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

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

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

9. dp数组的初值:+0,是否要long long,无穷要开够大

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

四边形不等式#

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

abcd,w(a,d)+w(b,c)w(a,c)+w(b,d)

或者

a<b,w(a,b+1)+w(a+1,b)w(a,b+1)+w(b,a+1)

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

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

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

对于fi,j=minik<j{fi,k+1+fk+1,j+w(i,j)},或者fi,j=mini1kj1{fi1,k+w(k+1,j)}fi,i=wi,i=0abcd,w(a,d)w(b,c)w满足四边形不等式,则f的决策数组p有二维决策单调性:

i<j,pi,j1<pi,j<pi+1,j or pi1,j<pi,j<pi,j+1

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

习题#

BZOJ 4709 柠檬#

CF868F Yet Another Minimization Problem#

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

首先列出方程:

fi,j=mink<j{fi1,k+jp=k+1jq=p+1[ap=aq]}

w(l,r)=ri=lrj=i+1[ai=aj],显然w(i,i)=fi,i=0,并且包含单调,现在我们要证明w(l,r)满足四边形不等式. 要证:

w(l,r+1)+w(l+1,r)w(l,r)+w(l+1,r+1)w(l,r+1)w(l,r)w(l+1,r+1)w(l+1,r)

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

f(l)=r+1i=lr+1j=i+1[ai=aj]ri=lrj=i+1[ai=aj]=ri=l(r+1j=i+1[ai=aj]rj=i+1[ai=aj])=ri=l[ar+1=ai]

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

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

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

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

Copy
#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#

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

fi,j=mink[0,j1]{fi1,k+(ajak+1)2}

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

w(i,j)=(ajai)2,那么要证明w(i,j+1)+w(i+1,j)w(i,j)+w(i+1,j+1),只要证明:w(i+1,j)w(i,j)w(i+1,j+1)w(i,j+1).

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

f(j)=(ajai+1)2(ajai)2=a2i+1a2i2aj(ai+1ai)

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

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

Copy
#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#

首先写出方程:

fi=minj[0,i1]{fj+(ik=1tk)(ik=1ckjk=1ck)+s(nk=1ckjk=1ck)}

用前缀和整理一下就是:

fi=minj[0,i1]{fj+Ti(CiCj)+s(CnCj)}

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

fjsCj_y=Ti_kCj_xTiCisCn+fi_b

然而Pj(Cj,fjsCj)没有任何单调性,不能直接用单调队列维护凸壳.

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

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

Copy
#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 @   Parsnip  阅读(437)  评论(0编辑  收藏  举报
编辑推荐:
· .NET 9 new features-C#13新的锁类型和语义
· Linux系统下SQL Server数据库镜像配置全流程详解
· 现代计算机视觉入门之:什么是视频
· 你所不知道的 C/C++ 宏知识
· 聊一聊 操作系统蓝屏 c0000102 的故障分析
阅读排行:
· DeepSeek V3 两周使用总结
· 回顾我的软件开发经历(1)
· C#使用yield关键字提升迭代性能与效率
· 低成本高可用方案!Linux系统下SQL Server数据库镜像配置全流程详解
· 4. 使用sql查询excel内容
点击右上角即可分享
微信分享提示
目录