我们知道,对一段连续区间进行修改/求和/求极值……操作,可用线段树等数据结构进行维护。那么,如果这不是一个区间,而是一棵树,我们又能怎样解决?此时我们就用到了树链剖分——这一专为此类的问题而生的算法。
树链剖分的原理是将一棵树划分为几部分并分别映射到一维区间中,这样对树进行操作就可以转化为对一维区间进行操作并用我们所熟悉的树状数组、线段树、Splay……维护。
最常用的树链剖分方法是轻重边树链剖分——将一棵树中的边分为轻重两类。首先对于每一个非叶子节点的节点,规定其重儿子为它的子节点中以其为根的子树节点总数最大的一个,上述节点与其重儿子之间的连边为一条重边,与其他子节点之间的连边为轻边,于是整棵树中每一条首尾相连的重边都构成了一条重链,每一条轻边都构成了一条轻链。
树链剖分过程代码实现:
首先对树进行第一遍DFS,求出每一个节点的父节点、深度、以其为根的子树的节点总数和重儿子,再进行第二遍DFS,即可求出每一个节点所在重链的顶端和它在区间中对应的位置、区间中每一点对应的树中的节点编号。
树链剖分过程C++代码实现:
void dfs(int x,int d)
{
size[x]=1;deep[x]=d;
for(int y,i=head[x];i;i=next[i])
if((y=to[i])!=fa[x])
{
fa[y]=x;
dfs(y,d+1);
if(size[y]>size[son[x]])
son[x]=y;
size[x]+=size[y];
}
}
void create(int x,int d)
{
p_id[x]=++now;
id_p[now]=x;
top[x]=d;
if(son[x])
create(son[x],d);
for(int y,i=head[x];i;i=next[i])
if((y=to[i])!=fa[x]&&y!=son[x])
create(y,y);
}
历经两遍DFS后整棵树便剖分完成了,每一条重链上的点都依次被映射到连续区间上,若对一条重链进行区间操作,则可使用线性结构维护,将时间由O(N)降到了O(logN)!另外,树链剖分的美妙性质:任意一点到根节点上的路径上的链的个数不超过logN条,从而对任意两点进行操作可通过枚举路径上重链进行操作而快速实现。下面列出求两点间路径上的点权和C++代码:
int findsum(int x,int y)
{
int f1=top[x],f2=top[y],re=0;
while(f1!=f2)
{
if(deep[f1]<deep[f2])
swap(x,y),swap(f1,f2);
re+=query_sum(1,1,n,p_id[f1],p_id[x]);
x=fa[f1];
f1=top[x];
}
if(deep[x]>deep[y])
swap(x,y);
return re+query_sum(1,1,n,p_id[x],p_id[y]);
}
上面代码中query_sum求同一条重链上两点间路径权值和(可用线段树维护)。
至此,树上区间操作问题被完美解决。
附模板题:【BZOJ1036】[ZJOI2008]树的统计Count(具体代码请参看本人其他博客)
【阅读上一篇:【算法杂谈_01】那些非主流排序算法】
【本文原创,转载请注明出处】