树链剖分及模板
在树上的两点之间最短路上,或是某一子树上完成一些整体的更新或求和的任务时,需要一些更快的算法。树链剖分的思想是,将树转化为链,也就是将二维转化为一维,然后用我们熟悉的线段树完成。
基本步骤
首先将树进行剖分,一般常用的是轻重边剖分,一遍dfs搞定。然后进行线段树预处理dfs,并处理好top。然后建造线段树,最后在操作的过程中利用top来完成更新或查询。
1.Dfs1——预处理
先给出一些定义:对于每一个节点,统计它的各个子树的节点个数,其中一个节点个数最多的子树称之为重儿子,其余为轻儿子。连接重儿子与自己的边叫重边,其余叫轻边。重边所连成的链称之为重链。
因此每个点都在且只在一条重链上。
在这一次DFS中,我们的主要任务就是统计出size
所以我们可以在Dfs的过程中统计出重边(重儿子),同时预处理出每个节点的深度,父亲,与子树中的节点个数。
int dfs1(int x, int father, int d){ dep[x] = d; fa[x] = father; size[x] = 1; int sz = G[x].size(), to,_mx=-1; for(int i = 0; i < sz; ++i){ to = G[x][i]; if(to == father) continue; size[x] += dfs1(to,x,d+1); if(size[to] > _mx){ _mx = size[to]; son[x] = to; } } return size[x]; }
2.Dfs2——剖分
我们现在要将这个数上的节点映射到线段树上,因此每个节点一定要对应一个线段树上叶子的编号,这样才能建立线段树。
由于我们已经标记好了size,我们希望一条重链上的节点对应的线段树的编号是连续的,这样就可以方便地更新了。如何来完成?只需要先DFS重儿子下去,这样时间戳自然就排好了。
另外,我们要计算top值。所谓top值也就是一个节点所在重链中深度最小的那一个节点。对于重儿子,top选择继承。对于轻儿子,top是它自己。由于每个点都在且只在一条重链上,所以每个点的top是唯一确定的。这个top有什么用?到第四步的时候再说
void dfs2(int x, int topf){ idx[x] = ++cnt; a[cnt] = w[x]; top[x] = topf; if(son[x]){ dfs2(son[x], topf); int sz = G[x].size(); for(int i = 0; i < sz; ++i){ if(!idx[G[x][i]]){ dfs2(G[x][i], G[x][i]); } } } }
3.线段树的建树
不再赘述,请看这里。
4.树上操作
树链剖分的重中之重。
(1)任意两点之间最短路径上的操作
树上任意两点之间的最短路是唯一确定,并且必定经过LCA。
这回我们可以利用到我们的top值了。选择两点中top较低的那一个点跳到top的上方,并且处理掉这一条被跳过的重链。然后在选一个再跳……直到这两个点在同一条重链上为止。最后再累积一下这一段即可。这样我们就通过了一次次的对链的操作完成了对树的操作。这就是我们为什么要求top,并且要求一条重链上的编号需要连续——因为我们每一次跳过的链都需要用线段树来更新或求解,这样就方便而快捷。
为什么是选择top值较低的来跳呢?回到我们的目的来看,我们的目的是要让这两个点最终跳到同一条链上,来完成操作。而在这之前,他们都低于他们的LCA。事实上,这就是在求解LCA。跳到同一条重链时上方的那个点就是LCA!所以树链剖分也可以轻松求解LCA(还不用打线段树哦~)
inline void TreeAdd(int x, int y, int z){ while(top[x] != top[y]){ if(dep[top[x]] < dep[top[y]]) Swap(x,y); Update(1,n,1,idx[top[x]],idx[x],z); x = fa[top[x]]; } if(idx[x] > idx[y]) Swap(x,y); Update(1,n,1,idx[x],idx[y],z); }
(2)子树上的操作
对于节点i的子树,在线段树里对应的编号一定是i ~ i + size[i] - 1,因为这一个子树的编号是一遍dfs完成的,所以一定是连续的。因此子树操作自然就解决了。