树链剖分详解

树链剖分是线段树的一个运用,也就是将一个树形结构的图转化到线段树中进行操作.

先来看一下树链剖分能解决哪些问题:

  1. 树上最短路径的修改.
  2. 树上最短路径的区间求和.
  3. 树上子树的修改.
  4. 树上子树的求和.

那么下面先介绍一些概念:

  1. 定义size(X)为以X为根的子树的节点个数
  2. 重儿子为一个节点的子节点中size值最大的节点
  3. 轻儿子为一个节点的非重儿子节点(一个节点有多个轻儿子)
  4. 重边是一个节点与重儿子的连边,轻边同理
  5. 重链是重边的连边,轻链同理

然后是需要记录的一些变量:

fa[]记录父亲,son[]记录重儿子,size[]记录节点的子节点个数,dep[]记录深度,top记录节点所在的当前链上的链顶,id[]记录在dfs序中的点权(树剖部分)

sum[]记录区间和,lazy记录懒惰标记(线段树部分)

last[]等数组记录链式前向星的建边,w[]记录点权

下面是基本思路:

  1. 第一遍dfs从根节点记录好每个节点的fa[],size[],son[],dep[],找出重儿子
  2. 第二遍dfs将重儿子连成重链(即记录每个点的top[]),同时记录每个点在dfs序下的权值,及dfs序
  3. 将每个点的dfs序作为编号加入线段树的建树中
  4. 在进行修改,查询时直接采用(类似)线段树的操作

于是先看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倍增的操作,在树上跳链,从一条链到另一条链上.

操作流程如下:

  1. 判断两个操作的点是否在同一条链上
  2. 如果不在同一条链上,则选一个深度更大的点向上跳,跳到链顶的父亲节点(这样必定会跳到另一条链上),并在沿途跳的路径进行要做的操作.
  3. 重复2,最终两个点会跳到同一条链上.
  4. 最后在同一条链上进行最后一次操作.
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的范围内.

原文地址:https://www.cnblogs.com/BCOI/p/8151103.html