树链剖分 - 知识点

一、定义

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

可以非常友(bao)好(li)的解决一些树上操作

(友情提示:学树链剖分之前请先掌握线段树)

二、思想

树链剖分的思想比较神奇

它的思想是:把一棵树拆成若干个不相交的链,然后用一些数据结构去维护这些链

那么问题来了,如何把树拆成链?

 


(1) 简单定义

首先明确概念:

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

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

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

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

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

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

 

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

2-11就是重链,2-5就是轻链,用红点标记的就是该结点所在重链的起点,也就是下文提到的top结点,

还有每条边的值其实是进行dfs时的执行序号。

const int maxn=1e5+10;
struct edge{
    int next;
    int to;
}e[2*maxn];

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

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

 

三、实现 


拆树的方法

对于每一个节点,找出它的重儿子,那么这棵树就自然而然的被拆成了许多重链与许多轻链

  •  如何对这些链进行维护?

首先,要对这些链进行维护,就要确保每个链上的节点都是连续的,

因此我们需要对整棵树进行重新编号,然后利用dfs序的思想,用线段树或树状数组等进行维护(具体用什么需要看题目要求,因为线段树的功能比树状数组强大,所以在这里我就不提供树状数组的写法了)

 注意在进行重新编号的时候先访问重链

这样可以保证重链内的节点编号连续

 

上面说的太抽象了,结合一张图来理解一下

对于一棵最基本的树

 

给他标记重儿子,

 蓝色为重儿子,红色为重边

然后对树进行重新编号

橙色表示的是该节点重新编号后的序号

不难看出重链内的节点编号是连续的

然后就可以在线段树上搞事情啦

像什么区间加区间求和什么的

另外有一个性质:以?i为根的子树的树在线段树上的编号为[?,?+子树节点数−1]


实现步骤:

1,对于一个点我们首先求出它所在的子树大小,找到它的重儿子(即处理出size,son数组),
解释:比如说点1,它有三个儿子2,3,4

2所在子树的大小是5

3所在子树的大小是2

4所在子树的大小是6

那么1的重儿子是4

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

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

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


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

dfs1()

这个dfs要处理几件事情:

  • 标记每个点的深度dep[]
  • 标记每个点的父亲fa[]
  • 标记每个非叶子节点的子树大小(含它自己)
  • 标记每个非叶子节点的重儿子编号son[]
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);

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

 

3,第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,并且为了用数据结构来维护重链,我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)

dfs2()

这个dfs2也要预处理几件事情

  • 标记每个点的新编号
  • 赋值每个点的初始值到新编号上
  • 处理每个点所在链的顶端
  • 处理每条链

顺序:先处理重儿子再处理轻儿子,理由后面说

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必然是它本身
    }
}

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

 

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;
}

大家如果明白了树链剖分,也应该有举一反三的能力(反正我没有),修改和LCA就留给大家自己完成了


例如:

在树上查询的这一步可能有些抽象,我们结合一个例子来理解一下

假设我们要查询3.6 这两个节点的之间的点权合,为了方便理解我们假设每个点的点权都是1

刚开始时

???[3]=2,???[6]=1

????[???[3]]=2,????[???[6]]=1

我们会让3向上跳,跳到???[3]的爸爸,也就是1号节点

这是1号节点和6号节点已经在同一条重链内,所以直接对线段树进行一次查询即可


5,树链剖分的时间复杂度
树链剖分的两个性质:

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值