长链剖分随想
之前写了那么长一篇Blog…现在不如写篇小短文…说一下另一种树链剖分方法——长链剖分的事情。它可以比重链剖分更快地完成一些东西。
树链剖分的原始版本重链剖分非常经典,这里就不从头介绍了。
原本的剖分方法是按照子树大小剖分,与子树点数最多的儿子连成链,所以叫做重链剖分…然后显然就有一个点到根的路径上至多条轻边这个性质(因为沿着轻边走,每次子树大小一定小于父亲的一半)。有了这个性质就可以做各种路径相关的查询,暴力每次跳到重链开头就好…
而在一些问题里,有这么一种奇妙的剖分方式可以取得更好的效果。那就是按照子树深度剖分,与最深的儿子连成链。之前一直不知道这个应该怎么叫,直到冬令营上听到敦敦敦提到“长链剖分”这个词,我才知道这个应该这么叫…
我所知道的长链剖分才能做的应用有两个,一个是统计每个点子树中可合并的以深度为下标的信息;另一个是经过一些预处理,单次在线查询一个点的级祖先。
先说一下第一个:统计每个点子树中可合并的以深度为下标的信息。(如某深度的点数,点权和,最值)
暴力的做法是的,因为一个点的多个子树的信息我们无法快速合并,合并复杂度可以达到。
但是我们对于重链剖分的方法可以想出一个的方法:自底向上统计,对于每个点,让它继承自己的重儿子的信息,然后我们暴力遍历其它子树并统计信息。这样做的话,每个点会在它到根路径上的条轻边被计算的时候被遍历,所以总复杂度是的。
看起来这个已经很优了,而且我们也用上了轻边数量这个性质,感觉没有浪费什么东西。再想想的话可以发现,其中遍历其它子树这一步有点浪费,因为我们统计的是可合并的以深度为下标的东西,我们其实只要循环一遍其他子树已经统计出来的信息就好了,这样我们的代价就不是子树大小而是深度了。但是这样还是不够的,复杂度没有变化。不过我们注意到继承重儿子这一点现在看起来就不是那么完美了,因为我们只需要深度为下标的信息,但是重链剖分是按照点数为标准的,所以我们可能继承了一个连出很多点但是深度很浅的扫把形重儿子,而其它轻儿子虽然点数不多,但是可能深度反而更深,所以可能选一个轻儿子更优。所以我们改变策略,选择继承子树最深的儿子的信息,然后循环其它子树的深度把信息统计到这个点上。
这样的复杂度是什么呢?如果我们仍然按照上面的方法分析,我们发现我们的复杂度可能不太对,因为到根路径上的轻边数量不再有保证了。但是如果我们换一种方法考虑就可以得到一个很好的复杂度。我们考虑每个子树被作为轻儿子暴力统计的代价,代价是它的深度,而它的深度其实就是它为顶端的长链的长度。每个点都是一个长链的开头,而所有长链都是不相交的,也就是说所有子树被作为轻儿子暴力统计的代价和是的。而被作为重儿子统计的代价,因为父亲直接继承了它的数组,所以每个点是的。于是我们就可以用的复杂度统计一棵树的每棵子树内的可合并的以深度为下标的信息。
然后是第二个:经过一些预处理,单次在线查询一个点的级祖先。
先说一下别的做法…可以离线的话,我们显然有一个非常水的总时间的单次询问的做法…DFS过程中直接找栈里的某一个即可。
不能离线的话也有一些传统做法。比如重链剖分,还是根据条轻链的性质,如果级组先就在当前重链上则直接找到,否则往上一条重链跳。复杂度
还有一种就是树上倍增,预处理出每个点的级祖先,然后询问的时候我们考虑的二进制表示,每次往为1的那些位的祖先上跳(可以看作用若干个的和表示)。这样预处理的复杂度是,询问的复杂度是。总体上比重链剖分还差。
还有另外一个相比起来复杂度比较糟糕的做法,就是记录一个点往上的前级祖先,询问的时候暴力往上跳,显然预处理的复杂度是,单次询问。
如果我们想突破到单次询问,我们依靠不了重链剖分这种经典做法。它对于它的思想来说已经相当优了,没有什么改进的空间。树上倍增的特点是,可以一步跳很远,但是如果要精准地跳到第级,就一定要遍历的每一个二进制位。而我们想出第三种做法的基本思路是,如果我们对每个点都维护它的所有祖先,我们就能回答询问。但是实际上的空间复杂度和预处理复杂度是不能承受的,于是我们折中地选择根号。
注意到如果我们查询的大于很多个的话,我们是一步一步跳级跳上去的,效率很低。这时我们其实可以考虑用树上倍增来优化。树上倍增要精准跳到级祖先复杂度比较高的问题可以用第三种做法的特点来弥补。如果在以内的话,我们可以跳过去。这样做的结果是什么呢?其实不太好,我们花了高昂的的代价预处理,得到的结果仅仅是查询时我们不再需要遍历最低的几个二进制位。
但是这种思想还是可以继续沿用的,接下来就是长链剖分出场的时候了。我们的目的是,让树上倍增进行尽量少的跳跃后就可以通过其他信息找到级祖先。我们首先可以像重链剖分一样维护一下每条长链,然后我们往上跑,求出长链顶端往上长链长度这么多级的祖先。这样做的时空复杂度仍然是。这样有什么用?这样做以后,我们的树上倍增只用跳最大的一步。
显然这个最大的一步的长度必定大于,于是我们跳到的那个点往下的长链长度至少就有,所以就算级祖先不在这条长链上,也一定可以从我们跳到的那个点的已知信息里直接求到(因为剩下的步数已经小于了,预处理的祖先长度往下的长链长度)。于是我们只要再预处理出对于每个数,它最大的二进制位是多少,我们就可以地求出任意一个点的级祖先了。(不过树上倍增的预处理复杂度仍然是,所以只有询问个数很多的时候才能有明显的效果)
我所知道的长链剖分的应用只有这两个…如果有人知道什么其它不用长链剖分的做法,或者长链剖分的其他的神奇应用的话,欢迎在下面留言。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现