树链剖分 参考

树链剖分详解_博客园_Ivanovcraft

*转载请注明出处,部分内容引自banananana大神的博客*

树链剖分

树刨的定义

树剖是通过轻重边剖分将树分割成多条链,然后利用数据结构来维护这些链(本质上是一种优化暴力)

定义

父亲结点:具有儿子结点的结点(儿子结点:具有父亲结点的结点)

重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;

轻儿子:父亲节点中除了重儿子以外的儿子;

  • 一个父亲结点只会偏宠爱一个儿子结点(唯一个优先继承人)。

重边:父亲结点和重儿子连成的边;

(连续去选择重儿子连接)

轻边:父亲节点和轻儿子连成的边;

重链:由多条重边连接而成的路径;

轻链:由多条轻边连接而成的路径【图论】树链剖分_数组

结点1有2,3,4三个儿子结点,其中以2为根节点的子树总共拥有5个结点,以3为根结点的子树一共拥有2个结点,以4为根节点的子树总共拥有6个结点。

因而作为父亲结点的1会优先选择结点4来作为自己的重儿子,而重儿子一旦选定,其他的儿子结点就是其轻儿子。

然后图中的粗黑线的关系代表的是父亲结点和重儿子结点之间的关系,即所谓的

重边,而其他相对细的边就为轻边

而我们可以取多条连续的重边,用它们来组成重链,比如1-4-9-13-14。

而我们可以取多条连续的轻边,用它们来组成轻链,比如1-2-5。

程序实现

由于我们要判断一个儿子结点是否为重儿子,因为我们就需要把每一个儿子结点所在的子树的包含的结点的个数给统计出来。

因而,我们需要开辟一个数组,名为size,用来统计子树的结点个数(其中size[u],代表的以u为根节点的子树的结点树),然后也不能白干活啊,因而我们需要对重儿子进行保留,开辟一个数组名为son(其中son[u]代表的是结点u的重儿子)。

  • 关于重儿子的讨论

如果一个点的多个儿子所在子树大小相等且最大

那随便找一个当做它的重儿子就好了

叶节点没有重儿子,非叶节点有且只有一个重儿子

然后再进行标记(跑dfs)的时候为了不回到父亲结点,我们可以再定义一个数组f(其中f[x]表示的是x结点的父亲结点)

然后这里已经做完了预处理操作了。

void dfs1(int u,int fa,int depth)
{
     f[u]=fa;//root结点的父亲结点可以令为-1或0
     d[u]=depth;//随着递归的深度而改变
     size[u]=1;//初始化本子树的结点数,然后再搜索的过程中给汇集起来
     for(遍历u的边)
     {
         提取边上的另一个端点v
         若是v是父亲结点就跳过
         否则往深继续跑dfs;
         size[u]+=size[v];//自下而上返还得到结点数
         if(size[v]>size[son[u]])//筛选出真正的重儿子
            son[u]=v;
     }
}
dfs1(root,0或者-1,1);

【图论】树链剖分_子树_02

dfs跑完大概是这样的,大家可以手动模拟一下

(逃命)

做个小的总结

数组名称 解释
f 表示当前结点的父亲结点,若其值为0或者-1,代表该点是root。
d 表示当前结点的深度,是父亲结点的深度加一,为了保证数组不越界,可使根节点的父亲结点为0。
size 表示以当前结点为根节点的子树的总结点树。
son 表示当前结点的重儿子,如果其值为0,表示当前结点为叶子结点,不具备有任何的儿子结点。

第一遍的dfs提前处理好每一个可行的结点的重儿子结点是哪一个,但仍还没有形成链,因而我们还需要对图再进行一次统计操作(再跑一遍dfs)

然后关于重链的探索,我们很清楚得知道得是当前结点连上它的重儿子结点可以形成一条重边,而这条重边就是重链的一个组成部分。而对于它的轻儿子来说,也不要灰心,它可能本身就是一条重链的顶端结点(为什么说是可能呢?因为这个结点可能是一个叶子结点,它是无法再继续形成一条重链的,也就是图中的结点5。

关于第二遍dfs我们需要准备的数组有top,(其中top[x]代表结点x的所在链的链头结点),也就是说如果有top[x]==top[y],则说明结点x和结点y是在同一条重链上。

  • son[x]==0代表结点x是叶子结点。
void dfs2(int u,int t)//当前结点,当前结点所处在的重链的顶端
{
     top[u]=t;
     //id[u]=++cnt;//dfs序,对点的重新标号,这步是为了后面线段树的操作
     //rk[cnt]=u;//从新标号重新映射回去到实际结点
     if(!son[u])
        reutrn ;
     dfs2(son[u],t);//再续良缘
     
     for(扫描u的边)
     {
          提取边上的另一个端点v
          如果v不是u的重儿子且不是u的父亲结点的话
             dfs2(v,v); //以轻儿子为新的一条可能的重链的顶端和新的当前结点dfs下去
     }
}

【图论】树链剖分_数组_03

第二遍dfs

数组名称 解释
top 当前结点所在的重链的链头结点
id 当前在dfs2实际访问得到的顺序,由于dfs2的设计,所以一条重链的id是连续的(这点这是线段树的可以用来维护重链的一个很重要的前提条件)
rk 通过dfs2序重新映射回起初的结点编号

两遍dfs跑完的结果。

dfs跑完大概是这样的,大家可以手动模拟一下

int sum(int x,int y)//代表的是x和y两个结点
{
     int ans=0,fx=top[x],fy=top[u];
     while(fx!=fy)//如果x和y不在同一条重链
     {
          if(d[fx]>=d[fy])//x的重链链头结点比y的重链链头结点的深度还低。
          {
               ans+=query(u,id[fx],id[x]);
               x=f[fx],fx=top[x];//将x设置其重链链头结点的父亲结点,fy为新结点所在重链的链头结点。
          }
          else
          {
               ans+=query(u,id[fy],id[y]);
               y=f[fy],fy=top[y];
          }
     }
     //while语句处理完后,x,y两个结点将同时处于一条重链上,也就意味这我们要单独去处理这两点之间的贡献
     if(id[x]<=id[y])//由于线段树区间的特性,要找到id比较小的,让它排到前面去
         ans+=query(u,id[x],id[y]);
     else 
         ans+=query(u,id[y],id[x]);
     return ans;
}