• 树链剖分详解


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

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

    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的范围内.

  • 相关阅读:
    UVA 707
    我写了一起 Makefile(一)
    百度mp3接口
    MBProgressHUD -[__NSCFString sizeWithAttributes:]: unrecognized selector问题解决了
    BCM策略路由交换芯片
    HDU 4006 The kth great number AVL解
    PHP进口Excel至MySQL方法
    隐藏Console形式无效(继续1)
    优秀的前端project如何制定一个老师--html学习路径
    地址阵列
  • 原文地址:https://www.cnblogs.com/BCOI/p/8151103.html
Copyright © 2020-2023  润新知