【暖*墟】 #数据结构进阶# 树链剖分

73 篇文章 0 订阅
14 篇文章 0 订阅

一. 树链剖分原理和实现

1.树链剖分的概念

用途:在一棵树上进行路径的修改、求极值、求和。

  • 树链,就是树上的路径
  • 剖分,就是把路径分类为 重链轻链
  • 树链剖分就是把一些点合成一条路径,使其在线段树中的下标有序,用线段树来维护。
  • 这样就可以使得查询、修改的效率大大提高。
  • 假设我们把路径分好链了,每次询问两个点对(x,y)时,
  • 若 x 和 y 在同一链中,直接询问线段树中的 u 和 v(因为同一条链中下标是连续的
  • 其中 u, v 是 x , y 对应的、线段树中的点。
  • 若不在同一链中,我们从深度大的点上一点一点向上爬(找fa),
  • 每次记录该点所在的链上的情况,直到 x, y在同一条链上即可。
  • 注意树链剖分中的线段树中每个点代表的意义可以是原图的 边 或 点 。

即:将树分割成多条链,然后利用数据结构(线段树、树状数组等)来维护链。

首先就是一些必须知道的概念:

  • 重结点:子树结点数目最多的结点;
  • 轻结点:父亲节点中除了重结点以外的结点
  • 重边:父亲结点重结点连成的边;
  • 轻边:父亲结点轻结点连成的边;
  • 重链:由多条重边连接而成的路径;
  • 轻链:由多条轻边连接而成的路径;

树链剖分

比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,

2-11就是重链,2-5就是轻链,用红点标记的就是该结点所在重链的起点,

(5,8,10结点单独成链),也就是下文提到的top结点。

算法中定义了以下的数组用来存储上边提到的概念:

名称解释
siz[u]保存以u为根的子树节点个数
top[u]保存当前节点所在链的顶端节点
son[u]保存重儿子
dep[u]保存结点u的深度值
faz[u]保存结点u的父亲节点
tid[u]保存树中每个节点剖分以后的新编号(DFS的执行顺序)

seg[u] ( 或rnk[u] )

保存当前节点在树中的位置(下标)

除此之外,还包括两种性质:

  1. 如果(u, v)是一条轻边,那么size(v) < size(u)/2;
  2. 从根结点到任意结点的路、所经过的轻重链的个数,必定都小于O(logn);

首先定义以下数组:

const int MAXN = (100000 << 2) + 10;

int siz[MAXN];//number of son

int top[MAXN];//top of the heavy link

int son[MAXN];//heavy son of the node

int dep[MAXN];//depth of the node

int faz[MAXN];//father of the node

int tid[MAXN];//ID -> DFSID

int rnk[MAXN];//DFSID -> ID

变量声明:

const int maxn=1e5+10;

struct edge{
    int next,to;
}e[2*maxn];

struct Node{
    int sum,lazy,l,r,ls,rs;
}node[2*maxn];

int rt,n,m,r,a[maxn],cnt;
int head[maxn],f[maxn],d[maxn],size[maxn];
int son[maxn],rk[maxn],top[maxn],id[maxn];

算法大致需要进行两次的DFS,第一次DFS可以得到当前节点的父亲结点(faz数组)、

当前结点的深度值(dep数组)、子结点数量(size数组)、重结点(son数组)

 

2.树链剖分的实现

1,对于一个点,首先求出它所在子树大小,找到它的重儿子(处理出size,son数组),

解释: 比如说点1,它有三个儿子2,3,4。2所在子树的大小是5,3所在子树的大小是2,

4所在子树的大小是6,那么1的重儿子是4。

ps: 如果一个点的多个儿子所在子树大小相等且最大,就随便选一个做重儿子。

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


2,在dfs过程中顺便记录其父亲及深度(处理出f,d数组),操作1,2可以通过一遍dfs完成。

void dfs1(int u,int fa,int depth){ //当前节点、父节点、层次深度
    f[u]=fa; d[u]=depth;
    size[u]=1; //这个点本身size=1
    for(int i=head[u];i;i=e[i].next){
        int v=e[i].to;
        if(v==fa) continue;
        dfs1(v,u,depth+1); //层次深度+1
        size[u]+=size[v]; //子节点的size已被处理,用它来更新父节点的size
        if(size[v]>size[son[u]]) son[u]=v; //选取size最大的作为重儿子
    }
}

dfs1(root,0,1);

 

3,第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,

并且为了用数据结构来维护重链,我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)

void dfs2(int u,int t){ //当前节点、重链顶端
    top[u]=t; id[u]=++cnt; //标记dfs序
    rk[cnt]=u; //序号cnt对应节点u
    if(!son[u]) return;
    dfs2(son[u],t);
    /* 我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
       一个点和它的重儿子处于同一条重链,
       所以重儿子所在重链的顶端还是t。   */
    for(int i=head[u];i;i=e[i].next){
        int v=e[i].to;
        if(v!=son[u]&&v!=f[u])
            dfs2(v,v); //一个点位于轻链底端,那么它的top必然是它本身
    }
}

 

4,两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,

那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息。

回顾上文的那个题目,修改和查询操作原理是类似的,以查询操作为例,其实就是个LCA,

不过这里使用了top来进行加速,因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,

他们的top就是自己。需要注意的是,每次循环只能跳一次,并且让结点深的那个来跳到top的位置,

避免两个一起跳从而插肩而过。

int sum(int x,int y){
    int ans=0,fx=top[x],fy=top[y];
    while(fx!=fy){    //两点不在同一条重链
        if(d[fx]>=d[fy]){
            ans+=query(id[fx],id[x],rt);    //线段树区间求和,处理这条重链的贡献
            x=f[fx],fx=top[x];    //将x设置成原链头的父亲结点,走轻边,继续循环
        }
        else{
            ans+=query(id[fy],id[y],rt);
            y=f[fy],fy=top[y];
        }
    }
    //循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点之间的贡献
    if(id[x]<=id[y]) ans+=query(id[x],id[y],rt);
    else ans+=query(id[y],id[x],rt);
    return ans;
}

 

5,树链剖分的时间复杂度

树链剖分的两个性质:

1,如果(u, v)是一条轻边,那么size(v) < size(u)/2;

2,从根结点到任意结点的路所经过的轻重链的个数必定都小于logn;

可以证明,树链剖分的时间复杂度为O(nlog^2n)

 

二. 例题练习

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值