浅谈树链剖分
引子
在OI中,有时候我们会需要处理一些树上的链的问题
比方说,给定一棵$n$个点的树,$m$个操作,每次查询$x$和$y$之间的链上的和
不要在意到底是什么操作,这真的只是个引子
考虑做法。
level 1
$ n,m \leq 100 $
那么我们直接暴力求即可。复杂度$O(nm)$
level 2
$ n\leq 1000 , m \leq 100000$
很明显不能直接暴力了。
询问过多,但是并没有修改操作,所以可以考虑$O(n^2)$把$x$和$y$之间的和预处理出来,存起来,直接访问。
level 3
$ n\leq10^5,m\leq10^5$,并且要求支持修改操作
这才是我们今天要讨论的问题
为了支持$O(log^2n)$的修改,$O(log^2n)$的查询,我们发展了这种叫树链剖分的东西。
树链剖分
前置芝士
DFS
线段树
链式前向星
定义
重儿子:一个点有多个儿子,定义其儿子中子树大小最大的儿子为重儿子。
轻儿子:一个点不是重儿子的儿子都是轻儿子。
重边:一个点与其重儿子之间的边。
轻边:一个点与其轻儿子之间的边。
重链:完全由重边连成的链。
重链的顶端:一条重链上深度最小(最靠近根)的点。
特别的,我们为了代码的舒适性人为定义一个轻儿子为一条长度为1的重链。
树链剖分能做什么?
解决一条链上的信息查询/修改问题
其他链上线段树能维护的东西
实现
存树
这个实际上不难,所有数字直接先丢进线段树的数组,边的话直接读入的时候链式前向星存下来就好。
考虑到是双向边,要加两次。
1 | inline void add(int u,int v){//链式前向星加边 |
两遍DFS
第一遍DFS
这遍DFS要处理出以下的信息:
一个点的深度$d$
一个点的重儿子$s$
一个点的父亲$f$
一个点的子树大小(含自己)$siz$
代码:
1 | inline void dfs1(int x){ |
第二遍DFS
在第一遍DFS的基础上,我们现在知道了每个点的子树大小(包括自己),重儿子等信息。
接下来就是树链剖分的核心了:
把一棵树按照轻重边剖分成若干条链,剖分的过程就是第二遍DFS
至于剖分的原因,后面在证明复杂度的时候会说
具体实现:
我们对每个点重标号,使得一条重链上的点的标号是连续的,然后对重标号后的点建线段树
第二遍DFS需要处理这些内容:
记录下每个点的新标号
把这个点的值赋到新标号上(之后建线段树要用)
记录下每个点所在的重链的顶端
代码:
1 | inline void dfs2(int x,int y){ |
处理
敲黑板:重点来了!也许你不清楚前文提到的两边DFS的意义,这里会有解释
我们进行了第二遍DFS之后,得到了一下的结果:
由于我们DFS时优先考虑重儿子,这样每条重链上的点的新标号是连续的
由于是DFS,每棵子树的新标号是连续的
链的查询/修改
有了上文,本来链上的问题变成了统计若干条重链加轻边的问题。
查询和修改的思路实际上很相似,都是两个点分别向上跳,直到在同一条重链上为止,最后直接统计一次重链上两个点之间的和。
既然重链上的新标号的连续的,那么我们就可以用线段树维护每条重链的和,这样重链的查询就是$O(logn)$的了。
至于轻边,我们可以直接暴力累加,但由于我们人为定义了每个轻儿子是一条长度为1的重链,所以实际上写代码的时候可以和重链的查询合并。
修改的代码:
1 | void chge(int x,int y,int k){ |
查询的代码:
1 | int query(int x,int y){ |
实际上很像,只是把线段树的修改换成了查询而已。
子树的修改/查询
既然有了一棵子树的新标号的连续的保证,那么原来的一棵子树实际上对应着线段树中的一段连续区间。
那么直接线段树区间修改/查询就好。而且由于是连续区间,那么$x$的子树的起点必然是$x$的新标号,终点必然是$x$的新标号$+x$的子树大小再$-1$。
那么代码就很简单了:
修改:
1 | tree.change(1,id[x],id[x]+siz[x]-1,1,n,k) |
查询:
1 | tree.ask(1,id[x],id[x]+siz[x]-1,1,n) |
复杂度简单证明
当树为二叉树的时候,深度最大。
一棵树总共有$n$个点,由于一个点$x$的重儿子起码占了$x$的子树大小的一半,那么每次递归下去节点个数都减半,很明显$logn$次就能结束。
这里特殊的是链的情况,然而整棵树上有一大部分是链的时候,显然他们必然连成若干条重链,而不可能都是轻边,那么对我们的复杂度并不构成巨大的影响。
所以重链的数量$\leq logn$。
又因为每两条重链之间必然由轻边分割(不然就连成一条重链了),所以轻边的数量同样$\leq logn$。
于是树链剖分做到了对于一条链上的查询/修改,$O(log^2n)$的复杂度。
很是优秀了。
完整代码
实际上讲到这里应该都会写了吧(雾,但为了讲清楚一些细节还是给一下吧
1 |
|