【8】树链剖分 学习笔记
2026/7/23 11:04:03 网站建设 项目流程

引入

树链剖分,顾名思义,就是将一棵树剖成一条条链。这种算法可以帮助我们快速解决一类和树上路径有关的的问题。

P3384 【模板】重链剖分 / 树链剖分

给定一棵n nn个点的树,需要支持q qq次询问,分为如下几种:

n , q ≤ 10 5 n,q \le 10^5n,q105


算法流程

既然知道了需要剖,那么怎么剖,以及剖好之后如何解决问题?下面我们详细说说。

怎么剖

首先看怎么剖。树剖有两种,一种是重链剖分,一种是长链剖分,后者用的比较少,这里我们讲前面这种。

对于树上每个点,我们记录其重儿子为其所有儿子中子树大小最大的那一个,重边为其连向重儿子的边。其余的则反过来,称为轻儿子和轻边。例如下图中的树:

像这种多个重边连起来的(如1 → 2 1 \to 2122 → 5 2 \to 5255 → 7 5 \to 757)(当然单个重边也是可以的)一条链就叫做重链,这条链最上面一个点(此处为1 11)就叫做该链的链头。对于一棵树上的任意一个点,我们有性质:

理解一下这个性质。

这些信息都可以用一次 DFS 求出。这样就完成了我们的剖分过程,下面我们来看看这个有什么用。

如何用

来回到例题,看看树剖咋用。

首先我们基本就不会啥树上的数据结构,考虑把这棵树拍平到序列上,这样我们的操作就比较好进行了。

咋拍呢,我们也没学过啥别的啊,就直接用 DFS 序吧。

但是这样我们重链的优秀性质就没了——那怎么行!我们想到一个折中方案:DFS 的时候,如果该点不是叶子,那么先遍历重儿子。这样,一条重链上的点的 DFS 序就是连续的了。

看起来问题差不多解决了,因为子树修和子树和都可以用 DFS 序拍成区间加和区间求和。链也可以,因为有性质,我们直接一直向上跳就可以了。

但是我们怎么知道跳到哪里是 LCA 呢?如果 LCA 在一个重链中间怎么办?这里有一个解决办法:我们每次选链头深度大的那边跳上去,直到x xxy yy在同一条重链上。因为 LCA 这个点至少有一个轻儿子(否则,x xxy yy就已经在一条重链了,这里可以自行思考),所以总有一个点会正好跳到 LCA 上面。当跳到一条重链上面的时候,此时一个点是 LCA,另一个点一定是其后代,此时这两个点之间的链也一定在原来的x → y x \to yxy路径上,而且 DFS 序也是连续的。

这就意味着,我们可以将一条链拆成序列上的O ( log ⁡ n ) O(\log n)O(logn)个连续区间,然后就可以直接线段树啦~

线段树就是区间加,区间求和就可以了。

分析一下复杂度:

代码比较好写。

:::info[Code]{open}

#include<bits/stdc++.h>usingnamespacestd;#defineintlonglongconstintN=1e5+5;inta[N],dfnid;vector<int>e[N];intsiz[N],son[N],dep[N],fa[N],dfn[N],top[N],d[N];structsegtree{inttree[4*N],tag[4*N];voidpushup(intx){tree[x]=tree[x*2]+tree[x*2+1];}voidpushdown(intx,intl,intr){intmid=(l+r)>>1;tree[x*2]+=(mid-l+1)*tag[x];tree[x*2+1]+=(r-mid)*tag[x];tag[x*2]+=tag[x];tag[x*2+1]+=tag[x];tag[x]=0;}voidbuild(intx,intl,intr){if(l==r)returntree[x]=a[d[l]],tag[x]=0,void();intmid=(l+r)>>1;build(x*2,l,mid);build(x*2+1,mid+1,r);pushup(x);}voidupdate(intx,intl,intr,intL,intR,intd){if(R<l||L>r)return;if(L<=l&&r<=R)returntree[x]+=(r-l+1)*d,tag[x]+=d,void();pushdown(x,l,r);intmid=(l+r)>>1;update(x*2,l,mid,L,R,d);update(x*2+1,mid+1,r,L,R,d);pushup(x);}intquery(intx,intl,intr,intL,intR){if(R<l||L>r)return0;if(L<=l&&r<=R)returntree[x];pushdown(x,l,r);intmid=(l+r)>>1;returnquery(x*2,l,mid,L,R)+query(x*2+1,mid+1,r,L,R);}}tr;voiddfs1(intnow,intf){fa[now]=f;dep[now]=dep[f]+1;siz[now]=1;intmxsz=0,mxid=0;for(autox:e[now]){if(x==f)continue;dfs1(x,now);siz[now]+=siz[x];if(siz[x]>mxsz)mxsz=siz[x],mxid=x;}son[now]=mxid;}voiddfs2(intnow,intf,intrt){dfn[now]=++dfnid;top[now]=rt;if(son[now])dfs2(son[now],now,rt);for(autox:e[now]){if(x==f||x==son[now])continue;dfs2(x,now,x);}}signedmain(){intn,m,r,p;cin>>n>>m>>r>>p;for(inti=1;i<=n;i++)cin>>a[i];for(inti=1,u,v;i<n;i++)cin>>u>>v,e[u].push_back(v),e[v].push_back(u);dfs1(r,0);dfs2(r,0,r);for(inti=1;i<=n;i++)d[dfn[i]]=i;tr.build(1,1,n);while(m--){intopt,x,y,z;cin>>opt>>x;if(opt==1){cin>>y>>z;while(top[x]!=top[y]){if(dep[top[x]]>dep[top[y]])swap(x,y);tr.update(1,1,n,dfn[top[y]],dfn[y],z);y=fa[top[y]];}if(dep[x]>dep[y])swap(x,y);tr.update(1,1,n,dfn[x],dfn[y],z);}elseif(opt==2){cin>>y;intsum=0;while(top[x]!=top[y]){if(dep[top[x]]>dep[top[y]])swap(x,y);sum+=tr.query(1,1,n,dfn[top[y]],dfn[y]);y=fa[top[y]];}if(dep[x]>dep[y])swap(x,y);sum+=tr.query(1,1,n,dfn[x],dfn[y]);cout<<sum%p<<endl;}elseif(opt==3){cin>>z;tr.update(1,1,n,dfn[x],dfn[x]+siz[x]-1,z);}else{cout<<tr.query(1,1,n,dfn[x],dfn[x]+siz[x]-1)%p<<endl;}}return0;}

:::

练习题 1

这些题目只需稍微转换一下题意,或改一下线段树即可。

边权转点权

有些题需要我们维护的是边权,而不是点权,怎么办呢?


P4315 月下“毛景树”

给定一棵树,进行如下几种操作:

n , q ≤ 10 5 n,q \le 10^5n,q105


边权直接处理起来挺麻烦的,能不能想个办法转成点权呢?当然可以!我们让每个边里深度较大的那个点作为“代表”,把边权换成这个点的点权就好了。修改和查询的时候,需要注意不要把 LCA 也改了,因为 LCA 存的是它上面那条边,所以最后一次更新的时候从 LCA 的儿子开始即可。


练习题 2

总结 & 后记

树链剖分是一种很常用(?的算法,可以和很多其他数据结构结合,灵活使用。

这篇主要是基础内容,后面如果需要可能(?会更进阶 qaq。


码字不易,能否给个赞 /wq ><

若对文章有任何问题和建议可以与作者私信交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询