树链剖分 详解 模板

·定义:树链剖分,它可以对一棵树进行轻重链剖分后用数据结构来维护每条重链,树链剖分的主要做法就是先把这颗树拆成一条一条链,那么对于每一条链,我们可以用线段树来维护,那么怎么拆这颗树呢,这才是树链剖分的要点。
·用途: 已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 将树从x到y结点最短路径上所有节点的值都加上z
操作2: 求树从x到y结点最短路径上所有节点的值之和
操作3: x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: x 表示求以x为根节点的子树内所有节点值之和
·一些定义与概念:
一、重链与重儿子
一个节点的重儿子,为其更大的一颗子树的根节点。从这个点连向重儿子的边我们称为重边。
这里我们定义子树的大小是取决于节点的数量,而和点权没有任何关系。
就比如在下图中:
在这里插入图片描述
红色的边就是重边。
我们一眼就可以发现——这些重边多多少少连成了一条链。而这些由重边连续连起来的点和边就组成了重链,也就是树链。
我们可以发现树链的一些性质——比如①一条树链一定是一个轻儿子或者根节点开头,通过重边串起来一些重儿子;②一个非叶子节点只有一个重儿子;③一个单独的叶子节点也是一条树链,【满足以轻儿子开头】,所以一棵树一定可以被划分为几条链等等。

那么重儿子怎么求呢,很简单,一个递归把这棵树遍历一遍就好了

void dfs_getson(int x)
{
    size[x]=1;//初始化树的大小 
    for(int i=first[x];i;i=e[i].next)
    {
        int y=e[i].y;
        if(y==fa[x])continue;//不能往回走 
        fa[y]=x;deep[y]=deep[x]+1;//记录父亲和深度 
        dfs_getson(y); 
        size[x]+=size[y];
        if(size[y]>size[son[x]])son[x]=y;//更新重儿子 
    }
}

在得到重儿子之后,我们再次进行一次DFS去寻找出每一条重链,那可能有人会问这棵树随便dfs一下都可以得到若干条dfs序连续的链呀,为什么一定要用重链?因为,分成重链可以使得每一条链尽可能长,并且分出来的链的数量大概是log(n)条,是比较优的。

void dfs_rewrite(int x,int tp)//当前点以及当前点所在重链的顶端
{
    top[x]=tp;//更新每个点所在的重链的顶端 
    now[x]=++tot;//得到每个点的dfs序(新编号) 
    past[tot]=x;//记录每个新编号原来是哪个点 
    if(son[x])dfs_rewrite(son[x],tp);//优先往重儿子那里走 
    for(int i=first[x];i;i=e[i].next)
    {
        int y=e[i].y;
        if(y!=son[x]&&y!=fa[x])dfs_rewrite(y,y);
    }
    ctr[x]=tot;//作用后面会讲 
}

然后我们现在考虑怎么利用这些结构来解决上面的问题,要知道x到y的路径,只需要模仿一下lca求公共祖先即可,但是要改一下,变成每次所在重链的顶端的深度更大的往上跳,直到它们在同一条重链上,为什么呢?为什么每次跳得不是深度更大的节点而是所在重链的顶端的深度更大的往上跳?因为,我们跳的方法是每次往当前节点所在的重链的top跳,然后再跳到父亲处(不跳多一次的话会永远停留在这个点),那么这就出现了一个问题,如果先跳深度更大的,有可能会跳过头。


void change_xtoy()//修改
{
    int x,y,z;
    scanf("%d %d %d",&x,&y,&z);
    while(top[x]!=top[y])//假如不在同一条重链上 
    {
        if(deep[top[x]]>deep[top[y])swap(x,y);//优先跳top深度更深的,将它存在y中 
        change(1,now[top[y]],now[y],z);//修改路径上的点的值 
        y=fa[top[y]];//往上跳 
    }
    if(deep[x]>deep[y])swap(x,y);
    change(1,now[x],now[y],z);//当他们在同一条重链上时,最后修改一下x~y路径上的点 
}

查询和也是类似的:

void getsum_xtoy()//查询,与修改基本相同
{
    int x,y;
    scanf("%d %d",&x,&y);
    ll ans=0;
    while(top[x]!=top[y])
    {
        if(deep[top[x]]>deep[top[y]])swap(x,y);
        ans=(ans+getsum(1,now[top[y]],now[y]))%p;
        y=fa[top[y]];
    }
    if(deep[x]>deep[y])swap(x,y);
    ans+=getsum(1,now[x],now[y]);
    printf("%lld\n",ans%p);
}

然后还剩下一个问题,子树怎么办?
因为以任意一个点作为根,它的子树内的点的dfs序都是连续的,所以我们只需要记录以每个节点为根的子树中新编号最大的那个节点的新编号即可,自己的新编号到最大的编号就是以自己为根的子树的新编号的范围。代码如下:

void change_sontree()
{
    int x,y;
    scanf("%d %d",&x,&y);
    change(1,now[x],ctr[x],y);//因为一颗子树的编号是连续的,所以直接修改即可(ctr前面有) 
}
void getsum_sontree()
{
    int x;
    scanf("%d",&x);
    printf("%lld\n",getsum(1,now[x],ctr[x])%p);//基本同上 
}

参考博客:https://blog.csdn.net/a_forever_dream/article/details/80651308;
参考视频:https://www.bilibili.com/video/av62060264?from=search&seid=8137962232453422249

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值