【学习笔记】树链剖分
1.树上差分
对于普通的序列差分,最后进行一次 \(dif_i=dif_i+dif_{i-1}\) 的操作使得所有点变回原来的样子。
但是树上差分不能这么干。因为我们进行普通差分时会让 \(dif_{r+1}\leftarrow dif_{r+1}-1\)。而在树上一个点可能有许多儿子,全部进行一次操作代码就很难搞,而且会被菊花图卡。
分析一下出问题的原因出在树上一个点可以有多个儿子。从而我们会想到在树上一个点还是只有一个祖先。于是我们再想到树上非常容易出现的递归性,就能想到最后进行的应该是 \(dif_i=\sum dif_e(i\ 是\ e\ 祖先)\)。
所以对于 \(dif_i\leftarrow dif_i+1\)。根据最后的求值,我们可以理解为从根到 \(i\) 的所有点加上 \(1\)。而我们只要从 \(e\) 到 \(i\) 的所有点加上 \(1\)。于是我们让 \(dif_{fa(e)}\leftarrow dif_{fa(e)}-1\)。
对于边差分就考虑把边下放到点。对于一个边 \((x,y)\),应该下放到深度更深的点,因为这个点只有一个父节点,从而代表只有这一条边连向了深度更浅的点。
那么这样的两条链应该拆成 \((l,x)\) 和 \((l,y)\)。操作就好。
注意点差分边差分修改的位置不同。
2.树上启发式合并(dsu on tree)
当题目中没有修改并且查询仅与子树有关可以考虑通过树上启发式合并来做到 \(O(n\log n)\)。
定义重儿子为子树中节点最多的儿子。轻儿子是除重儿子外的儿子。一个点连向重儿子的边叫做重边,其他的叫做轻边。
考虑这样一个暴力:每次把一个子树的答案算出来,然后清空所有数组。显然复杂度是 \(O(n^2)\) 的。
考虑优化这个暴力。我们发现对于子树 \(x\),最后一个计算的儿子的贡献不需要清空,因为自己反正还要用一遍。
那么我们可以考虑把最大的儿子(重儿子)留下来不删除。也就是在计算当前点的时候,先把轻儿子答案计算完,并且计算完就删掉防止和其他儿子产生冲突,然后计算重儿子答案并保留贡献,再把所有轻儿子贡献加回来得到当前答案。
实现的话我们给函数多加上一个布尔值代表这一次的数据需不需要保留。然后后面给轻儿子加贡献就写一个新的函数。
复杂度是 \(O(n\log n)\)。
- 证明:
考虑一个点会被计算的次数。如果它位于 \(f\) 的重儿子中它的贡献会被传上去,那么一直是统计一遍。然后如果碰到轻边会重新计算一次,也就是说一个点被计算的次数是它到根的轻边数加一。
考虑一条边是轻边,那么节点数至少乘以 \(2\),所以轻边数是 \(\log n\) 的。所以复杂度 \(O(n\log n)\)。
3.重链剖分
重链是连续的重边连成的链。
根据上面的性质我们知道重链的断开点是轻边,一共只有 \(\log n\) 个轻边,所以重链数量级是 \(\log n\) 的,而且大多数情况下搞不满。
根据这个性质,对于重链剖分,它能维护很多东西。前提是 dfs 序,在 dfs 时优先访问重儿子能够保证重链 dfs 序连续,然后子树 dfs 序也连续,所以它能配合很多可爱的数据结构进行很多可爱的算法。
针对子树的操作很简单,对于路径操作,我们维护当前点所在的重链顶,然后找两个端点里面深度最深的点往上面跳直到位于同一个重链(具体原因忘了,有博客讲过),过程中根据数据结构更新。
4.长链剖分
类似重链剖分。把子树中深度最大的点作为重儿子。一个点到根节点的重边切换次数是 \(O(\sqrt n)\) 级别的。
常见的用处如 \(O(n\log n)-O(1)\) 求 k 级祖先。做法挺好玩的,自己看题解。