『动态规划的单调性优化』
动态规划的单调性优化#
决策集合优化#
dp的时候决策集合只扩大不减小,直接把最大值/最小值/累加和记下来就好了.
例如:LCIS CH5101,fi,j=max0≤k<j,Bk<Ai{fi−1,k}+1.
外层i不变,随着j增大,每次决策k最多增加一个,判断一下是否合法记下来即可.
单调队列优化#
dp的时候决策区间上下界都单调变化,可以直接记录区间和的值. 如果是最优化dp,那就要用滑动窗口记录区间最值.
写代码的时候,在循环开始的时候排除队首过期决策,然后取队首作为最优解,然后加入新决策. 有些边界麻烦的初值可以暴力算掉.
多重背包怎么用单调队列优化?先写方程:f[j]=max1≤k≤ci{f[j−k×vi]+k×wi}.
观察:决策点每次位移vi,那就把j按照vi的余数分类. 设j=u+vi×p,于是:
这样的话可以把i,u当成定值,枚举p,此时决策变量k的上下界单调变化,就可以用单调队列了.
斜率优化#
多项式v(i,j)含有i,j乘积项的1D/1D动态规划,一般可以用单调队列维护下凸壳/上凸壳.
设fi=max0≤j≤i−1{fj+A(i)+B(j)+p(i)q(j)},然后移项一下:
这时候把所有决策点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),若其满足:
或者
则称w满足四边形不等式.
对于fi=min0≤j<i{fj+w(j,i)},w(a,b)满足四边形不等式,则f的决策数组p单调不减.
暴力优化的话直接从上一个决策点开始枚举即可,如果用队列维护决策点连续段可以做到O(nlog2n),需要用二分找分界点. 如果是二维的dp,每次仅从上一维转移,可以采用分治写法,时间复杂度也是每层O(nlog2n),如果一维dp强行套cdq分治的话,时间复杂度两个log.
对于fi,j=mini≤k<j{fi,k+1+fk+1,j+w(i,j)},或者fi,j=mini−1≤k≤j−1{fi−1,k+w(k+1,j)}若fi,i=wi,i=0,∀a≤b≤c≤d,w(a,d)≥w(b,c),w满足四边形不等式,则f的决策数组p有二维决策单调性:
按照长度为阶段的区间dp,直接在决策范围里面枚举时间复杂度就优化到O(n2),序列分段型的dp每段倒序更新也可以优化到O(n2).
习题#
BZOJ 4709 柠檬#
CF868F Yet Another Minimization Problem#
还没来得及做这题就被出在考试里了...
首先列出方程:
设w(l,r)=∑ri=l∑rj=i+1[ai=aj],显然w(i,i)=fi,i=0,并且包含单调,现在我们要证明w(l,r)满足四边形不等式. 要证:
设f(l)=w(l,r+1)−w(l,r),展开:
显然有f(l)≥f(l+1),所以w满足四边形不等式,原动态规划具有决策单调性.
现在,我们显然可以通过桶来O(n)计算一次转移的贡献,但是这样太慢了,似乎也没有什么好的办法.
一个想法是用类似于莫队的指针移动的方法优化,不过直接利用决策单调性倒序转移,指针移动的量还是O(n). 不妨考虑决策单调性的分治算法.
我们可以让区间的指针一开始移动到分治决策区间的左右端点,那么对于分治函数d(l,r,L,R),其指针移动的量就是O(R−L)的,递归的时候把它移到对应的位置,移动的量也是O(R−L),所以根据分治的复杂度分析,由(l,r)可以控制分治层数不超过O(log2n)层,且分治树上相同深度节点的时间贡献和为O(∑(R−L))=O(n),那么总复杂度是不变的,也就是O(knlog2n),可以通过.
#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段的最小权值和.于是就有:
看到这里,你已经可以无脑斜率优化过掉这题了. 不过我们要讨论一下另一个东西:决策单调性.
设w(i,j)=(aj−ai)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),显然有:
显然ai+1−ai为正,则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),比斜率优化好写,细节也更少.
#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#
首先写出方程:
用前缀和整理一下就是:
如果要斜率优化,就整理成:
然而Pj(Cj,fj−sCj)没有任何单调性,不能直接用单调队列维护凸壳.
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;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET 9 new features-C#13新的锁类型和语义
· Linux系统下SQL Server数据库镜像配置全流程详解
· 现代计算机视觉入门之:什么是视频
· 你所不知道的 C/C++ 宏知识
· 聊一聊 操作系统蓝屏 c0000102 的故障分析
· DeepSeek V3 两周使用总结
· 回顾我的软件开发经历(1)
· C#使用yield关键字提升迭代性能与效率
· 低成本高可用方案!Linux系统下SQL Server数据库镜像配置全流程详解
· 4. 使用sql查询excel内容