「学习笔记」分块,莫队,复杂度平衡

「学习笔记」分块,莫队,复杂度平衡

各类根号算法。不太适合当作复习所用,就当写个教程了,可能因为这个东西不大需要复习(

关于复杂度分析:在实践中还是不要套式子,应当直接分析。

关于块长:在实践中还是不要套式子,应当直接调块长看怎样跑得快。

分块

有时候不同操作的数量不同,比方说一般的普通莫队,会有 O(nn) 次修改,但是只有 O(n) 次查询,这个时候若采用 O(1) 修改,O(n) 查询,就会得到 O(nn) 的优秀复杂度!

常用来复杂度平衡的分块:

O(n) 区间修改 O(1) 单点查询

序列分块,整块打标记,散块暴力修改。

O(1) 单点修改,O(n) 区间求和

序列分块,维护整块总和,散块暴力查。

O(n) 区间加,O(1) 区间求和

对于每个块维护块内前缀/后缀和,维护整块之间的前缀和。

修改:对于块内前缀后缀和:整块打标记,散块暴力重构;整块之间的前缀和暴力修改。

查询:整块为两个前缀和相减,散块查这个块里面的前缀后缀和。

O(1) 区间加,O(n) 区间求和

类似上一个,对于每个块维护块内差分,维护整块之间的差分。

更好写的做法:差分一下,O(1) 单点修改,区间求和变成两个前缀求和,那么就只需要求 O(n) 求前缀 (x+1i)×ai 的值,维护 aiai×i 即可。

O(1) 添加一个数,O(n) 查询第 k

值域分块,维护整块里有多少个数,询问的时候直接从小到大遍历整块直到不够的时候再查散块。

O(n) 添加一个数,O(1) 查询第 k

对排名这一维分块,然后对于每个块用 deque 维护一个排序序列。插入的时候找到这个数所在的 deque 暴力插,后面的 deque 是若干个 push_front 和 pop_back。

查询的话直接定位该排名到所在的块,查询 deque 里第 k 个值就可以了(deque 的定位是 O(1) 的)。

莫队

我的理解:莫队是在高维上利用分块的一种扫描线。或者说,在高维上的若干点的最小生成树上 dfs。

理想莫队信息:维护一个子集的信息,支持 O(a) 插入一个元素,O(b) 删除一个元素,无法比直接暴力插入更高效地合并。

普通莫队

设块长为 B,易分析得出端点移动次数为 n2B+mB,若 n,m 同阶,取 B=n 最优。但实际上,根据数据生成的方式不同,不同的块长在效率上也有所不同,但我们这里暂且讨论复杂度上问题,不考虑数据不同带来的影响。

莫队掌握的经典技巧是复杂度平衡。

如果 m<n,取块长 nm,移动次数和查询次数得到平衡,左右端点移动次数和查询次数均为 O(nm),这一步是根据均值不等式来得出的。

举个栗子,例如每次端点移动是区间修改,查询是单点查询,n,m 同阶。这个时候如果用线段树或者树状数组,复杂度为 O(nnlogn).但是注意到询问只有 m 次,询问的复杂度比修改的复杂度还要低,尝试平衡修改和询问的复杂度,采用序列分块来做到 O(1) 修改 n 查询,这样总复杂度就是 O(nn)

还有诸如把 log 放进根号里面的技巧,当然这都是理论上的东西,在实践中,由于实现不同部分的常数差异,块长有时候取得更大或者更小是更优的。

下面整理的几种莫队我认为都是更偏向于 trick 或者套路而并非模板。

带修莫队 & 高维莫队

啊这个玩意是不是在时间上多了一维的莫队!

如果有修改,那么就加上一维时间,这里令 n,m 同阶,故分析时所有 m 均由 n 替代。

按照 B 为块长分块,共有 nB 个块。排序的时候先按照左端点所在块排序,再按照右端点所在块排序,再按照时间排序。然后三个指针去扫。

  • 左右指针所在块编号不变时,块之内的时间指针移动次数是 O(n),块之间的时间指针移动次数也是 O(n);左右指针所在块编号的种数是 O((nB)2),所以时间指针移动总复杂度是 O(n2B)

  • 左指针所在块编号不变时,对于相邻两个询问,右指针在块之内移动次数是 O(B),在块之间移动次数是 O(B);左指针所在块编号改变时,右指针移动 O(n),左指针所在块编号改变 O(nB) 次。所以右指针移动的总复杂度为 O(nB+n2B)

  • 左指针移动次数易分析得 O(nB)

所以总时间复杂度为 O(nB+n2B),根据均值不等式,取 nB=n2B 时,即 B=n23 最优,总时间复杂度为 O(n53)

一般化:高维莫队

方法:对前 k1 维分块,按照第 i 关键字为第 i 个维度的所在块编号排序,第 k 维关键字为第 k 维坐标。

尝试用扫描线的角度来一般化在 k 个维度上做莫队的过程,这里假设指针的移动是 O(1) 的,每一处指针移动次数都要和 mmin

考虑第 k 维指针的总移动次数:块数乘上 n 也就是 O(nkBk1)

对于第 i 维,考虑前 i1 维在同一个块内,第 i 维指针的移动次数:它每次移动 O(B) 次,一共移动 O(mB) 次。

对于第 i 维,考虑前 i1 维在不同块之间第 i 维指针的移动次数:

  • 从一个块到下一个相邻的块,每次移动 O(B) 次,一共移动 O(mB) 次;

  • 从最后一个块到第一个块,每次移动 O(n) 次,仅会在前面维度的块改变的时候才会移动,前面维度的块数为 O(ni1Bi1),所以这部分移动次数是 O(niBi1),向上松一松就是 O(knkBk1),由于通常 k 不是很大,那就视这个 k 为常数。

综上所述,指针移动次数为 O(nkBk1+mB),同样根据均值不等式得到 B=nm1k 时最优,时间复杂度为 (nmk1k)

树上莫队

查询的信息:

  • 链信息
  • 子树信息

其中如果查询的子树信息是理想莫队信息,那么直接启发式合并即可。

链上查询信息的方法:

  • 信息可以差分,一条链差分成四个点到根的路径(四维降成二维)
  • 对于连通块进行分块(树分块)
  • 对于括号序分块

理想莫队信息,第一种方法就不行了,第三种方法相比于第二种代码难度和常数都更优。

那么解决思路大概就是,把树的信息用括号序变成序列,然后在上面跑莫队。

括号序

假设点 x 的入栈时间为 inx,出栈时间为 outx

对于括号序上的一段区间,如果它仅出现一次,那么就统计上它的信息。

假设现在要询问树上路径 uv 的信息。如果它们互为祖先关系,假设 uv 的祖先,那么这条路径的信息就相当于括号序 [inu,inv] 的信息。

如果它们不互为祖先关系,假设 outu<inv,那么这条路径的信息就相当于 [outu,inv] 的信息。

括号序还可以用来判断两点间是否存在祖先关系,判断 u 是否是 v 的祖先等价于判断 [inu,outu] 是否完全包含 [inv,outv]

这样在括号序上跑莫队就可以维护信息了。

回滚莫队

在正常的问题中,普通莫队是不断移动左右端点来维护答案。但有的问题,插入和删除复杂度是不一样的。有时候插入更快一些,有时候删除更快一些。采用只插入不删除,或者只删除不插入的莫队,称之为回滚莫队。

仍是按照左端点所在的块为第一关键字,按照右端点为第二关键字排序。

只插入莫队:初始左端点为所在块右端点,右端点从小往大扫,每次处理询问的时候直接撤销左端点的影响,重新从块的右端点开始移动。

只删除莫队:类似地,初始左端点为所在块左端点,右端点从大往小扫,每次处理询问的时候直接撤销左端点的影响,重新从块的左端点开始移动。

很多博客都是让左右端点在同一块内的暴力扫,对于只删除莫队,如果散块询问不好暴力扫得出,一并加入到最后扫莫队也是不影响的。

二次离线莫队

咋一看上去很高深,其实并不难,其实也是一种思想?

例题:多次询问区间逆序对,允许离线(洛谷 P5047)

很容易想到一个 莫队+树状数组 的做法。每次移动指针的时候用树状数组来计算答案的变化。一共有 O(nm) 次单点修改,区间查询。

注意到这个过程本质上还是在做数点,那把区间查询差分成前缀查询,然后把这些询问离线下来,做数点。这样就只有 O(n) 次单点修改,O(nm) 次区间查询,用 O(n) 单点修改,O(1) 区间查询,就可以做到 O(n(n+m)) 的时间复杂度。

但是卡空间,而空间复杂度为 O(nm),优化空间的话,假设是右端点向右移动(其余移动情况同理),每次都是询问 ([1,r1],r) 产生的逆序对,而这个可以预处理出来之后作前缀和来 O(1) 得到;还有询问 ([1,l1],r) 的逆序对,注意到 l1 是固定的,那么把 [r,qr] 两个区间端点挂在 l1 处即可。这样空间就是线性的了。

小结

前面提到莫队本质上是离线下来作扫描线,而把莫队指针移动的的过程中所做的修改和询问操作,再离线下来,通过扫描线等技巧优化复杂度处理,就是二次离线了!

根号重构

本节整理自 lxl 的课件。

本质上是对时间分块。

基于这么一个套路:

  • O(x) 重构整个序列;
  • O(y) 计算一个修改操作对一次查询的影响;
  • 每隔 t 个修改重构整个序列。

对于查询,其总复杂度为 O(mty);对于重构,其总复杂度为 O(mxt)

经典问题

维护一棵树,支持断边,加边,链 kth。

考虑根号重构,每 m 重构一次。

这样询问就转化成查询 m 段树链总的 kth。

用可持久化线段树维护,每次询问在这 m 个可持久化线段树上一块儿二分。

查询复杂度 O(mlogn),重构复杂度 O(nlogn),总复杂度 O((n+m)mlogn)

小结

lxl 的话:

在复杂的范围修改查询问题中,可以考虑根号重构。

这里是一个统计规律。

实际上我认为是对于复杂的范围修改查询问题,维之间是不对称的,时间维是结构最简单的,对其进行分块容易出好的效果。

我暂且还没有那么深的理解,没有深刻理解到“维之间不对称”,对于“时间维结构较简单”稍微有那么一点感觉(?),还需要多做点题。

我对后半句理解大概就是,时间维的结构是简单的,因此尝试在时间维上维护信息。类比序列上的线段树和树状数组,在时间维上维护信息的方法有线段树分治和根号重构。

Trick 和例题

ain,不同的 ai 只有 O(n) 种。

先咕着

posted @   do_while_true  阅读(520)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通

This blog has running: 1841 days 20 hours 16 minutes 27 seconds

点击右上角即可分享
微信分享提示