树链剖分

来源:互联网 发布:v380监控软件安卓版 编辑:程序博客网 时间:2024/06/06 19:49

前言:

树链剖分,就是将树剖成一条条链,然后通过数据结构(如Treap,线段树,树状数组,Splay,LCT或者各种树套树)维护这些链,从而快速完成路径的权的统一修改,增加,求极值等。

其实,以前关于树剖的博客,我已经写过了。。。但由于写得太难看,最后删掉了。。。现在把它补上。


初一参加的GDOI和GDKOI,听了不止两遍的树链剖分(好像都是关于第4题的),可每次都听不懂。那时我的心情是这样的:


这就是树链剖分了。。。


好吧,不说废话了。直接进入主题:

先定义Size值为以该节点为树根的子树的节点个数。

重儿子:是该节点所有儿子中Size值最大的儿子。而与重儿子相连的边就叫重边,其余的就叫轻边了。

如上图,用重边组成一条条重路径

可以知道,从根节点到某一节点u,经过的轻边和重路径不会超过条。

理由:先证明树链剖分一个重要的性质:

如果有任何一个节点u和其轻儿子v,都有

证明:

反证法:假设

则对于u的重儿子v1,有:

左边两式相加:

可得:

与Size的定义矛盾,假设不成立,故原结论成立。


有了这个定理,问题不难解决:

如果从根节点走到u,最多经过条轻边,因为每经过一次轻边,Size值至少没了一半。


好了,接下来的问题来了,该如何用数据结构(后以线段树为例)来维护重路径的操作?每一条重路径都建一个线段树?显然不是。我们可以将每一个点(或者边)都给其重新编号,使相邻的重边的编号相邻。那么,只需dfs即可,先dfs重儿子,再dfs轻儿子,然后给上编号。为了确定重儿子,在这之前还要搞一次dfs以确定重儿子。


那么,该如何实现路径的这些操作?使两个点一边靠近它们的LCA,一边进行操作。

1,如果两个点还不在同一条重路径上,对深度较深的点进行操作。

可以从该节点x到重路径的顶端top[x]用线段树进行修改(或查询),并暴力修改(查询)top[x]和fa[top[x]]的这条轻边。x跳到fa[top[x]],并重复该操作。

2,如果两点已在同一条重路径上了,那么用线段树修改(查询)这两个点之间边(或点)。


至此,我们需要用到的数组有:

size[]:以该节点为根节点的子树的节点个数

dep[]:该节点在树中的深度

fa[]:该节点的父亲

son[]:该节点的重儿子

top[]:该节点所在重路径中深度最小的点

tid[]:该点(边)在线段树中的位置。


两次dfs的过程如下【仅供参考】:
void dfs1(int u,int p)//第一次Dfs {vis[u] = true;dep[u] = dep[p] + 1;//计算深度 fa[u] = p;//父亲 siz[u] = 1;//初始化Size值 int len = adj[u].size();for(int i=0;i<len;i++){int v = adj[u][i];if((!vis[v]) && (v != fa[u])){dfs1(v,u);siz[u] += siz[v];//计算Size值 if(son[u] == 0) son[u] = v;//先取第一个儿子为重儿子 else if(siz[son[u]] < siz[v]) son[u] = v;//然后去Size值最大的点 }}}void dfs2(int u,int tp)//第二次dfs {vis[u] = true;tim ++;tid[u] = tim;top[u] = tp;//计算top if(son[u]) dfs2(son[u],tp);//先dfs重儿子 int len = adj[u].size();for(int i=0;i<len;i++)//然后dfs其他儿子 {int v = adj[u][i];if((!vis[u]) && (v != fa[u] && v != son[u]))//注意需判断该节点是否不为重儿子 dfs2(v,v);}}