各类根号算法。不太适合当作复习所用,就当写个教程了,可能因为这个东西不大需要复习(
关于复杂度分析:在实践中还是不要套式子,应当直接分析。
关于块长:在实践中还是不要套式子,应当直接调块长看怎样跑得快。
分块
有时候不同操作的数量不同,比方说一般的普通莫队,会有 次修改,但是只有 次查询,这个时候若采用 修改, 查询,就会得到 的优秀复杂度!
常用来复杂度平衡的分块:
区间修改 单点查询
序列分块,整块打标记,散块暴力修改。
单点修改, 区间求和
序列分块,维护整块总和,散块暴力查。
区间加, 区间求和
对于每个块维护块内前缀/后缀和,维护整块之间的前缀和。
修改:对于块内前缀后缀和:整块打标记,散块暴力重构;整块之间的前缀和暴力修改。
查询:整块为两个前缀和相减,散块查这个块里面的前缀后缀和。
区间加, 区间求和
类似上一个,对于每个块维护块内差分,维护整块之间的差分。
更好写的做法:差分一下, 单点修改,区间求和变成两个前缀求和,那么就只需要求 求前缀 的值,维护 和 即可。
添加一个数, 查询第 大
值域分块,维护整块里有多少个数,询问的时候直接从小到大遍历整块直到不够的时候再查散块。
添加一个数, 查询第 大
对排名这一维分块,然后对于每个块用 deque 维护一个排序序列。插入的时候找到这个数所在的 deque 暴力插,后面的 deque 是若干个 push_front 和 pop_back。
查询的话直接定位该排名到所在的块,查询 deque 里第 个值就可以了(deque 的定位是 的)。
莫队
我的理解:莫队是在高维上利用分块的一种扫描线。或者说,在高维上的若干点的最小生成树上 dfs。
理想莫队信息:维护一个子集的信息,支持 插入一个元素, 删除一个元素,无法比直接暴力插入更高效地合并。
普通莫队
设块长为 ,易分析得出端点移动次数为 ,若 同阶,取 最优。但实际上,根据数据生成的方式不同,不同的块长在效率上也有所不同,但我们这里暂且讨论复杂度上问题,不考虑数据不同带来的影响。
莫队掌握的经典技巧是复杂度平衡。
如果 ,取块长 ,移动次数和查询次数得到平衡,左右端点移动次数和查询次数均为 ,这一步是根据均值不等式来得出的。
举个栗子,例如每次端点移动是区间修改,查询是单点查询, 同阶。这个时候如果用线段树或者树状数组,复杂度为 .但是注意到询问只有 次,询问的复杂度比修改的复杂度还要低,尝试平衡修改和询问的复杂度,采用序列分块来做到 修改 查询,这样总复杂度就是 .
还有诸如把 放进根号里面的技巧,当然这都是理论上的东西,在实践中,由于实现不同部分的常数差异,块长有时候取得更大或者更小是更优的。
下面整理的几种莫队我认为都是更偏向于 trick 或者套路而并非模板。
带修莫队 & 高维莫队
啊这个玩意是不是在时间上多了一维的莫队!
如果有修改,那么就加上一维时间,这里令 同阶,故分析时所有 均由 替代。
按照 为块长分块,共有 个块。排序的时候先按照左端点所在块排序,再按照右端点所在块排序,再按照时间排序。然后三个指针去扫。
-
左右指针所在块编号不变时,块之内的时间指针移动次数是 ,块之间的时间指针移动次数也是 ;左右指针所在块编号的种数是 ,所以时间指针移动总复杂度是 .
-
左指针所在块编号不变时,对于相邻两个询问,右指针在块之内移动次数是 ,在块之间移动次数是 ;左指针所在块编号改变时,右指针移动 ,左指针所在块编号改变 次。所以右指针移动的总复杂度为 .
-
左指针移动次数易分析得 .
所以总时间复杂度为 ,根据均值不等式,取 时,即 最优,总时间复杂度为 .
一般化:高维莫队
方法:对前 维分块,按照第 关键字为第 个维度的所在块编号排序,第 维关键字为第 维坐标。
尝试用扫描线的角度来一般化在 个维度上做莫队的过程,这里假设指针的移动是 的,每一处指针移动次数都要和 取 .
考虑第 维指针的总移动次数:块数乘上 也就是 ;
对于第 维,考虑前 维在同一个块内,第 维指针的移动次数:它每次移动 次,一共移动 次。
对于第 维,考虑前 维在不同块之间第 维指针的移动次数:
-
从一个块到下一个相邻的块,每次移动 次,一共移动 次;
-
从最后一个块到第一个块,每次移动 次,仅会在前面维度的块改变的时候才会移动,前面维度的块数为 ,所以这部分移动次数是 ,向上松一松就是 ,由于通常 不是很大,那就视这个 为常数。
综上所述,指针移动次数为 ,同样根据均值不等式得到 时最优,时间复杂度为
树上莫队
查询的信息:
- 链信息
- 子树信息
其中如果查询的子树信息是理想莫队信息,那么直接启发式合并即可。
链上查询信息的方法:
- 信息可以差分,一条链差分成四个点到根的路径(四维降成二维)
- 对于连通块进行分块(树分块)
- 对于括号序分块
理想莫队信息,第一种方法就不行了,第三种方法相比于第二种代码难度和常数都更优。
那么解决思路大概就是,把树的信息用括号序变成序列,然后在上面跑莫队。
括号序
假设点 的入栈时间为 ,出栈时间为 .
对于括号序上的一段区间,如果它仅出现一次,那么就统计上它的信息。
假设现在要询问树上路径 的信息。如果它们互为祖先关系,假设 是 的祖先,那么这条路径的信息就相当于括号序 的信息。
如果它们不互为祖先关系,假设 ,那么这条路径的信息就相当于 的信息。
括号序还可以用来判断两点间是否存在祖先关系,判断 是否是 的祖先等价于判断 是否完全包含 .
这样在括号序上跑莫队就可以维护信息了。
回滚莫队
在正常的问题中,普通莫队是不断移动左右端点来维护答案。但有的问题,插入和删除复杂度是不一样的。有时候插入更快一些,有时候删除更快一些。采用只插入不删除,或者只删除不插入的莫队,称之为回滚莫队。
仍是按照左端点所在的块为第一关键字,按照右端点为第二关键字排序。
只插入莫队:初始左端点为所在块右端点,右端点从小往大扫,每次处理询问的时候直接撤销左端点的影响,重新从块的右端点开始移动。
只删除莫队:类似地,初始左端点为所在块左端点,右端点从大往小扫,每次处理询问的时候直接撤销左端点的影响,重新从块的左端点开始移动。
很多博客都是让左右端点在同一块内的暴力扫,对于只删除莫队,如果散块询问不好暴力扫得出,一并加入到最后扫莫队也是不影响的。
二次离线莫队
咋一看上去很高深,其实并不难,其实也是一种思想?
例题:多次询问区间逆序对,允许离线(洛谷 P5047)
很容易想到一个 莫队+树状数组 的做法。每次移动指针的时候用树状数组来计算答案的变化。一共有 次单点修改,区间查询。
注意到这个过程本质上还是在做数点,那把区间查询差分成前缀查询,然后把这些询问离线下来,做数点。这样就只有 次单点修改, 次区间查询,用 单点修改, 区间查询,就可以做到 的时间复杂度。
但是卡空间,而空间复杂度为 ,优化空间的话,假设是右端点向右移动(其余移动情况同理),每次都是询问 产生的逆序对,而这个可以预处理出来之后作前缀和来 得到;还有询问 的逆序对,注意到 是固定的,那么把 两个区间端点挂在 处即可。这样空间就是线性的了。
小结
前面提到莫队本质上是离线下来作扫描线,而把莫队指针移动的的过程中所做的修改和询问操作,再离线下来,通过扫描线等技巧优化复杂度处理,就是二次离线了!
根号重构
本节整理自 lxl 的课件。
本质上是对时间分块。
基于这么一个套路:
- 重构整个序列;
- 计算一个修改操作对一次查询的影响;
- 每隔 个修改重构整个序列。
对于查询,其总复杂度为 ;对于重构,其总复杂度为 .
经典问题
维护一棵树,支持断边,加边,链 kth。
考虑根号重构,每 重构一次。
这样询问就转化成查询 段树链总的 kth。
用可持久化线段树维护,每次询问在这 个可持久化线段树上一块儿二分。
查询复杂度 ,重构复杂度 ,总复杂度 .
小结
lxl 的话:
在复杂的范围修改查询问题中,可以考虑根号重构。
这里是一个统计规律。
实际上我认为是对于复杂的范围修改查询问题,维之间是不对称的,时间维是结构最简单的,对其进行分块容易出好的效果。
我暂且还没有那么深的理解,没有深刻理解到“维之间不对称”,对于“时间维结构较简单”稍微有那么一点感觉(?),还需要多做点题。
我对后半句理解大概就是,时间维的结构是简单的,因此尝试在时间维上维护信息。类比序列上的线段树和树状数组,在时间维上维护信息的方法有线段树分治和根号重构。
Trick 和例题
,不同的 只有 种。
先咕着
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通