首页 > 代码库 > 长链剖分随想
长链剖分随想
之前写了那么长一篇Blog…现在不如写篇小短文…说一下另一种树链剖分方法——长链剖分的事情。它可以比重链剖分更快地完成一些东西。
树链剖分的原始版本重链剖分非常经典,这里就不从头介绍了。
原本的剖分方法是按照子树大小剖分,与子树点数最多的儿子连成链,所以叫做重链剖分…然后显然就有一个点到根的路径上至多$O(\log n)$条轻边这个性质(因为沿着轻边走,每次子树大小一定小于父亲的一半)。有了这个性质就可以做各种路径相关的查询,暴力每次跳到重链开头就好…
而在一些问题里,有这么一种奇妙的剖分方式可以取得更好的效果。那就是按照子树深度剖分,与最深的儿子连成链。之前一直不知道这个应该怎么叫,直到冬令营上听到敦敦敦提到“长链剖分”这个词,我才知道这个应该这么叫…
我所知道的长链剖分才能做的应用有两个,一个是$O(n)$统计每个点子树中可合并的以深度为下标的信息;另一个是经过一些预处理,单次$O(1)$在线查询一个点的$k$级祖先。
先说一下第一个:$O(n)$统计每个点子树中可合并的以深度为下标的信息。(如某深度的点数,点权和,最值)
暴力的做法是$O(n^2)$的,因为一个点的多个子树的信息我们无法快速合并,合并复杂度可以达到$O(n)$。
但是我们对于重链剖分的方法可以想出一个$O(n \log n)$的方法:自底向上统计,对于每个点,让它继承自己的重儿子的信息,然后我们暴力遍历其它子树并统计信息。这样做的话,每个点会在它到根路径上的$O(\log n)$条轻边被计算的时候被遍历,所以总复杂度是$O(n \log n)$的。
看起来这个已经很优了,而且我们也用上了轻边数量这个性质,感觉没有浪费什么东西。再想想的话可以发现,其中遍历其它子树这一步有点浪费,因为我们统计的是可合并的以深度为下标的东西,我们其实只要循环一遍其他子树已经统计出来的信息就好了,这样我们的代价就不是子树大小而是深度了。但是这样还是不够的,复杂度没有变化。不过我们注意到继承重儿子这一点现在看起来就不是那么完美了,因为我们只需要深度为下标的信息,但是重链剖分是按照点数为标准的,所以我们可能继承了一个连出很多点但是深度很浅的扫把形重儿子,而其它轻儿子虽然点数不多,但是可能深度反而更深,所以可能选一个轻儿子更优。所以我们改变策略,选择继承子树最深的儿子的信息,然后循环其它子树的深度把信息统计到这个点上。
这样的复杂度是什么呢?如果我们仍然按照上面的方法分析,我们发现我们的复杂度可能不太对,因为到根路径上的轻边数量不再有保证了。但是如果我们换一种方法考虑就可以得到一个很好的复杂度。我们考虑每个子树被作为轻儿子暴力统计的代价,代价是它的深度,而它的深度其实就是它为顶端的长链的长度。每个点都是一个长链的开头,而所有长链都是不相交的,也就是说所有子树被作为轻儿子暴力统计的代价和是$O(n)$的。而被作为重儿子统计的代价,因为父亲直接继承了它的数组,所以每个点是$O(1)$的。于是我们就可以用$O(n)$的复杂度统计一棵树的每棵子树内的可合并的以深度为下标的信息。
然后是第二个:经过一些预处理,单次$O(1)$在线查询一个点的$k$级祖先。
先说一下别的做法…可以离线的话,我们显然有一个非常水的总时间$O(n+q)$的单次询问$O(1)$的做法…DFS过程中直接找栈里的某一个即可。
不能离线的话也有一些传统做法。比如重链剖分,还是根据$O(\log n)$条轻链的性质,如果$k$级组先就在当前重链上则直接找到,否则往上一条重链跳。复杂度$O(\log n)$
还有一种就是树上倍增,预处理出每个点的$2^0,2^1,2^2,...,2^{\log n}$级祖先,然后询问的时候我们考虑$k$的二进制表示,每次往为1的那些位的祖先上跳(可以看作用若干个$2^i$的和表示$k$)。这样预处理的复杂度是$O(n \log n)$,询问的复杂度是$O(\log n)$。总体上比重链剖分还差。
还有另外一个相比起来复杂度比较糟糕的做法,就是记录一个点往上的前$\sqrt{n}$级祖先,询问的时候暴力往上跳,显然预处理的复杂度是$O(n \sqrt{n})$,单次询问$O(\sqrt{n})$。
如果我们想突破到单次询问$O(1)$,我们依靠不了重链剖分这种经典做法。它对于它的思想来说已经相当优了,没有什么改进的空间。树上倍增的特点是,可以一步跳很远,但是如果要精准地跳到第$k$级,就一定要遍历$k$的每一个二进制位。而我们想出第三种做法的基本思路是,如果我们对每个点都维护它的所有祖先,我们就能$O(1)$回答询问。但是实际上$O(n^2)$的空间复杂度和预处理复杂度是不能承受的,于是我们折中地选择根号。
注意到如果我们查询的$k$大于很多个$\sqrt{n}$的话,我们是一步一步跳$\sqrt{n}$级跳上去的,效率很低。这时我们其实可以考虑用树上倍增来优化。树上倍增要精准跳到$k$级祖先复杂度比较高的问题可以用第三种做法的特点来弥补。如果$k$在$\sqrt{n}$以内的话,我们可以$O(1)$跳过去。这样做的结果是什么呢?其实不太好,我们花了高昂的$O(n \sqrt{n})$的代价预处理,得到的结果仅仅是查询时我们不再需要遍历最低的几个二进制位。
但是这种思想还是可以继续沿用的,接下来就是长链剖分出场的时候了。我们的目的是,让树上倍增进行尽量少的跳跃后就可以通过其他信息找到$k$级祖先。我们首先可以像重链剖分一样维护一下每条长链,然后我们往上跑,求出长链顶端往上长链长度这么多级的祖先。这样做的时空复杂度仍然是$O(n)$。这样有什么用?这样做以后,我们的树上倍增只用跳最大的一步。
显然这个最大的一步的长度必定大于$k/2$,于是我们跳到的那个点往下的长链长度至少就有$k/2$,所以就算$k$级祖先不在这条长链上,也一定可以从我们跳到的那个点的已知信息里直接求到(因为剩下的步数已经小于$k/2$了,预处理的祖先长度$=$往下的长链长度$>k/2$)。于是我们只要再预处理出对于每个数,它最大的二进制位是多少,我们就可以$O(1)$地求出任意一个点的$k$级祖先了。(不过树上倍增的预处理复杂度仍然是$O(n \log n)$,所以只有询问个数很多的时候才能有明显的效果)
我所知道的长链剖分的应用只有这两个…如果有人知道什么其它不用长链剖分的做法,或者长链剖分的其他的神奇应用的话,欢迎在下面留言。
长链剖分随想