树剖

· · 个人记录

介绍

树剖主要作用可以理解为对一棵树进行类似于线段树的操作。(废话)

主要是通过一些神奇操作将一棵树转化为多个链(可以理解为多个线段树),并且在这多个链中反复跳跃,完成以下几个操作:

(其实还有些操作比如求最大值之类的,不过都可自行理解)

那么问题来了,我们如何在各个链间反复跳跃呢?

主要内容

  1. 几个概念

    重儿子(可理解为很重要的儿子,因为真的很重要(雾)):一个结点的重儿子,就是以此结点的各个儿子为根的子树中,结点最多的那棵树的根。

    轻儿子:显而易见,不是重儿子的结点就是轻儿子。

    重边:一个结点连接其重儿子的边。

    轻边:依然显而易见,不是重边的边就是轻边。

    重链:几条相连重边组成的链。

    • 注:重链起点均为轻儿子。

    直接上图

    由这个图则可以看出一些有趣的东西:

    • 除了重链就是轻儿子叶节点,而这些轻儿子叶结点也会单独构成重链(虽然只有一个结点而没边,但它就是重链)

    • 看似毫无意义的轻边作用为连接不同的链

  2. 几个数组

  1. 几个函数

    • dfs1 :初始化fa,deep,son数组。
    inline void dfs1(int x,int f) //x为当前结点,f为父亲结点
    {
         size[x]=1,deep[x]=deep[f]+1,fa[x]=f;
         for(int i=p[x].first;i;i=p[i].next)
         {
             int y=p[i].go;
             if(y==f) continue;//作为无向图,防止在两个结点中反复横跳
             dfs1(y,x);
             size[x]+=size[y];//显而易见,各个子树大小的和加上根这一结点就是此树的结点数
             if(size[y]>size[son[x]]) son[x]=y; //不断比较求得重儿子 
         }
    }
    • dfs2 :初始化id,di,top数组。
    int dotot;//dfs树搜索顺序 
    inline void dfs2(int x,int topf) //x为当前结点,topf是此链的开头结点
    {
         id[x]=++dotot,di[dotot]=x,top[x]=topf;
         if(!son[x]) return ;//搜到叶节点就返回
         dfs2(son[x],topf);//这里是优先搜重儿子
         for(int i=p[x].first;i;i=p[i].next)
         {
             int y=p[i].go;
             if(y!=son[x]&&y!=fa[x]) dfs2(y,y);//当一个儿子结点没有top时,则此结点非重儿子,那么就是一条新链的起点 
         }
    }

    再次强调,这里优先搜重儿子,具体体现在下图

    • build && down && update && answer :原线段树的四个函数,用于对一棵树进行修改与查询,因写法只需改动少许即放到一处处理。
    //线段树与大众写法略有不同,不过不影响树剖学习 
    inline void build(int k,int x,int y)
    {
         l[k]=x,r[k]=y;
         if(x==y)
         {
             imp[k]=ord[di[x]];//此处即用上di数组
             return ;
         }
         int mid=(x+y)>>1;
         build(k<<1,x,mid);
         build(k<<1|1,mid+1,y);
         imp[k]=imp[k<<1]+imp[k<<1|1]; 
    }
    inline void down(int k)
    {
         if(!add[k]) return ;
         add[k<<1]+=add[k];
         add[k<<1|1]+=add[k];
         imp[k<<1]+=(r[k<<1]-l[k<<1]+1)*add[k];
         imp[k<<1|1]+=(r[k<<1|1]-l[k<<1|1]+1)*add[k];
         add[k]=0;
    }//毫无变化
    inline void update(int k,int x,int y,int a)
    {
         if(x<=l[k]&&y>=r[k])
         {
             add[k]+=a;
             imp[k]+=(r[k]-l[k]+1)*a;
             return ;
         }
         down(k);
         if(x<=r[k<<1]) update(k<<1,x,y,a);
         if(y>=l[k<<1|1]) update(k<<1|1,x,y,a);
         imp[k]=imp[k<<1]+imp[k<<1|1];
    }//毫无变化
    inline int answer(int k,int x,int y)
    {
         if(x<=l[k]&&y>=r[k]) return imp[k];
         down(k);
         int ans=0;
         if(x<=r[k<<1]) ans+=answer(k<<1,x,y);
         if(y>=l[k<<1|1]) ans+=answer(k<<1|1,x,y);
         return ans;
    }//毫无变化 
    • upchain && anschain : 相对线段树所新增的两个函数,实现两点间最短路的修改与查询。放在一块处理也是因为差别不大
    inline void upchain(int x,int y,int a)
    {
         while(top[x]!=top[y])//将x(较深的点)向上爬到和y(较浅的点)同一条链 
         {
             if(deep[top[x]]<deep[top[y]]) swap(x,y);//保持x为较深点便于操作 
             update(1,id[top[x]],id[x],a);//更新x向上爬时经过的点,每次更新一条链
             x=fa[top[x]];//跳到下一条链 
         }
         if(deep[x]>deep[y]) swap(x,y);
         update(1,id[x],id[y],a);//此时两点在同一条链上,则直接修改 
    }
    //其实修改和查询区别就两行。。。。 
    inline void anschain(int x,int y)
    {
         int ans=0; 
         while(top[x]!=top[y])
         {
             if(deep[top[x]]<deep[top[y]]) swap(x,y);
             ans+=answer(1,id[top[x]],id[x]);
             x=fa[top[x]];
         }
         if(deep[x]>deep[y]) swap(x,y);
         ans+=answer(1,id[x],id[y]);
         return ans;
    }
    
  2. 主函数

    主函数要处理的则是关于这四个问题的函数调用。

    1. 树修改&&树查询:
     update(1,id[x],id[x]+size[x]-1,a);
     answer(1,id[x],id[x]+size[x]-1);

    对于 id[x]+size[x]-1 这一部分,其实在dfs2时就可见:一棵子树的各个结点所对应在dfs树上的序号是一个区间内递增的。

    对这句话的理解,举例子: 一颗子树的序号可以为

    • 2,3,4,5
    • 5,6,7,8,9,10,11
    • 6

    (应该举例就很好理解了)

    配合图理解

    2.两点修改&&两点查询:

    upchain(x,y,a);
    anschain(x,y);

    这倒是很直白。