动态规划的单调性优化
决策集合优化
(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),于是:
这样的话可以把(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)}),然后移项一下:
这时候把所有决策点(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)),若其满足:
或者
则称(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)有二维决策单调性:
按照长度为阶段的区间(mathrm{dp}),直接在决策范围里面枚举时间复杂度就优化到(O(n^2)),序列分段型的(mathrm{dp})每段倒序更新也可以优化到(O(n^2)).
习题
BZOJ 4709 柠檬
CF868F Yet Another Minimization Problem
还没来得及做这题就被出在考试里了(...)
首先列出方程:
设(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))满足四边形不等式. 要证:
设(f(l)=w(l,r+1)-w(l,r)),展开:
显然有(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)段的最小权值和.于是就有:
看到这里,你已经可以无脑斜率优化过掉这题了. 不过我们要讨论一下另一个东西:决策单调性.
设(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)),显然有:
显然(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
首先写出方程:
用前缀和整理一下就是:
如果要斜率优化,就整理成:
然而(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;
}