树链剖分详解
树链剖分是线段树的一个运用,也就是将一个树形结构的图转化到线段树中进行操作.
先来看一下树链剖分能解决哪些问题:
- 树上最短路径的修改.
- 树上最短路径的区间求和.
- 树上子树的修改.
- 树上子树的求和.
那么下面先介绍一些概念:
- 定义size(X)为以X为根的子树的节点个数
- 重儿子为一个节点的子节点中size值最大的节点
- 轻儿子为一个节点的非重儿子节点(一个节点有多个轻儿子)
- 重边是一个节点与重儿子的连边,轻边同理
- 重链是重边的连边,轻链同理
然后是需要记录的一些变量:
fa[]记录父亲,son[]记录重儿子,size[]记录节点的子节点个数,dep[]记录深度,top记录节点所在的当前链上的链顶,id[]记录在dfs序中的点权(树剖部分)
sum[]记录区间和,lazy记录懒惰标记(线段树部分)
last[]等数组记录链式前向星的建边,w[]记录点权
下面是基本思路:
- 第一遍dfs从根节点记录好每个节点的fa[],size[],son[],dep[],找出重儿子
- 第二遍dfs将重儿子连成重链(即记录每个点的top[]),同时记录每个点在dfs序下的权值,及dfs序
- 将每个点的dfs序作为编号加入线段树的建树中
- 在进行修改,查询时直接采用(类似)线段树的操作
于是先看dfs1吧
也没啥很难的操作,大概就是一个找重儿子的过程,其他都简单.先上代码:
1 void dfs1(int x,int deep,int f){ 2 dep[x]=deep;fa[x]=f;int maxson=-1;//每层递归中保留一个maxson和节点数比较,用于找重儿子 3 for(int i=last[x];i;i=e[i].next){ 4 int to=e[i].to; 5 if(to!=f){ 6 dfs1(to,deep+1,x); 7 size[x]+=size[to]; 8 if(maxson<size[to]){//找重儿子的步骤 9 maxson=size[to]; 10 son[x]=to; 11 } 12 } 13 } 14 }
在递归中定义的maxson可以每层都保存一个值,找重儿子很方便.
dfs2
dfs2的操作是把每个重儿子连成一条条的重链,方便后面的操作(之后会讲).
连重链事实上就是记录下每个点所在链的链顶,并且记录下第二遍dfs中每个节点进入搜索的时间戳(方便作为编号加入线段树).
在连重链的时候先连重链,然后回溯上来再连轻链(因为每个非叶子节点必定有一个重儿子,所以这样可以遍历整张图).下面是代码:
1 void dfs2(la x,la tp){ 2 id[x]=++idx;tx[idx]=w[x];top[x]=tp;//tx[]记录在时间戳中第idx个点的权值,id[]记录每个点的dfs序 3 if(!son[x]) return; 4 dfs2(son[x],tp);//按重儿子搜到底,连完一条重链 5 for(la i=last[x];i;i=e[i].next){ 6 la to=e[i].to; 7 if(to==fa[x]||to==son[x]) continue; 8 dfs2(to,to);//然后处理轻链,轻链的链顶就是自己 9 } 10 }
线段树
线段树的操作可以看一下之前一篇博客的讲解,然后在树剖中就是把每个节点按照它在dfs2中的顺序作为编号加入线段树中.
1 void build(int root,int left,int right){ 2 if(left==right){ 3 sum[root]=tx[left]; 4 return; 5 } 6 build(ll(root),left,mid); 7 build(rr(root),mid+1,right); 8 sum[root]=sum[ll(root)]+sum[rr(root)]; 9 return; 10 }
这样加入线段树之后,就会有一些性质:
同一条重链上的点是连成一段一段加入线段树中的,且链顶最先加入线段树,该链深度最深的节点id[x]=id[top[x]]+size[top[x]]-1;
那么将信息加入了线段树中,要怎么对树进行操作呢?
于是这里有了一个类似于lca倍增的操作,在树上跳链,从一条链到另一条链上.
操作流程如下:
- 判断两个操作的点是否在同一条链上
- 如果不在同一条链上,则选一个深度更大的点向上跳,跳到链顶的父亲节点(这样必定会跳到另一条链上),并在沿途跳的路径进行要做的操作.
- 重复2,最终两个点会跳到同一条链上.
- 最后在同一条链上进行最后一次操作.
void chainupdata(int a,int b,int val){ while(top[a]!=top[b]){//流程1 if(dep[top[a]]<dep[top[b]]) swap(a,b);//默认a为深度更深的点 updata(1,1,n,id[top[a]],id[a],val);//在线段树中修改一个点到链顶 a=fa[top[a]];//继续向上跳,直到两个点跳到同一条重链上 } if(id[a]>id[b]) swap(a,b);//在同一条链上后,最后修改 updata(1,1,n,id[a],id[b],val); } int chainquery(int a,int b){ la res=0; while(top[a]!=top[b]){ if(dep[top[a]]<dep[top[b]]) swap(a,b); (res+=query(1,1,n,id[top[a]],id[a]))%=mod; a=fa[top[a]]; } if(id[a]>id[b]) swap(a,b); (res+=query(1,1,n,id[a],id[b]))%=mod; return res;//同理 }
在链上的操作只有这些.
然后根据剖出树的性质,可以得出对子树进行操作的方法:
1 int ans = query(1,1,n,id[x],id[x]+size[x]-1);
修改同理.
这样做的原因是因为在dfs2中打上时间戳的顺序,使得一个节点的子树中所有点的时间戳都在id[x],id[x]+size[x]-1的范围内.