简述
树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。
具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。
重链剖分
重链剖分可以将树上的任意一条路径划分成不超过 \(O(\log n)\) 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。
重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。
如:
- 修改 树上两点之间的路径上 所有点的值。
- 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)。
定义
- 重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子
- 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
- 叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
- 重边:连接任意两个重儿子的边叫做重边
- 轻边:剩下的即为轻边
- 重链:相邻重边连起来的 连接一条重儿子 的链叫重链
- 对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
- 每一条重链以轻儿子为起点
code
namespace qwq{ //树剖部分
//第一个dfs(init)记录父节点(fa)、深度(dep)、子树大小(sz)、重子节点(son)
int dep[N],fa[N],son[N],sz[N];
il void init(int u,int pre){
int mxson=-1;
dep[u]=dep[pre]+1,fa[u]=pre,sz[u]=1;
for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
int v=edge::e[i].v;
if(v==pre) continue;
init(v,u),sz[u]+=sz[v];
if(sz[v]>mxson) mxson=sz[v],son[u]=v;
}
return;
}
//第二个dfs(getnum)记录所在链的链顶(top)、重边优先遍历的dfs序(dfn)
int top[N],dfn[N],cnt,wi[N]; //点权(wi)可不用……
il void getnum(int u,int tp){
top[u]=tp,dfn[u]=++cnt,wi[cnt]=w[u];
if(!son[u]) return;
getnum(son[u],tp); //先处理重儿子
for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
int v=edge::e[i].v;
if(v==fa[u]||v==son[u]) continue;
getnum(v,v); //再处理轻儿子
}
return;
}
} using namespace qwq;
应用
最近公共祖先
不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。
向上跳重链时需要先跳所在重链顶端深度较大的那个。
il int lca(int u,int v){
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]]) u=fa[top[u]];
else v=fa[top[v]];
}
return dep[u]>dep[v]?v:u;
}
维护路径
链上的 DFS 序是连续的,可以使用线段树、树状数组维护。
每次选择深度较大的链往上跳,直到两点在同一条链上。
同样的跳链结构适用于维护、统计路径上的其他信息。
namespace operate{
il void op1(int x,int y,int z){ //将树从x到y结点最短路径上所有节点的值都加上z。
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
add(1,1,n,dfn[top[x]],dfn[x],z),x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return add(1,1,n,dfn[x],dfn[y],z);
}
il int op2(int x,int y){ //求树从x到y结点最短路径上所有节点的值之和
int as=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
as=(as+ask(1,1,n,dfn[top[x]],dfn[x]))%mod;
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return as=(as+ask(1,1,n,dfn[x],dfn[y]))%mod;
}
}
维护子树
子树中的结点的 DFS 序是连续的。
节点\(x\)子树所在连续区间为\([dfn[x],dfn[x]+sz[x]-1]\)。
这样就把子树信息转化为连续的一段区间信息。
namespace operate{
il void op3(int x,int z){ //将以x为根节点的子树内所有节点值都加上z
return add(1,1,n,dfn[x],dfn[x]+sz[x]-1,z);
}
il int op4(int x){ //求以x为根节点的子树内所有节点值之和
return ask(1,1,n,dfn[x],dfn[x]+sz[x]-1);
}
}
AC·code
#include<bits/stdc++.h>
#define il inline
#define cs const
#define ri register
using namespace std;
cs int N=1e5+5;
int n,m,root,mod,w[N];
namespace edge{
int h[N];
struct node{int v,nxt;}e[N*2];
il void add(int u,int v,int id){
e[id*2-1]={v,h[u]},h[u]=id*2-1;
return e[id*2]={u,h[v]},h[v]=id*2,void();
}
}
namespace qwq{ //树剖部分
int dep[N],fa[N],son[N],sz[N];
il void init(int u,int pre){
int mxson=-1;
dep[u]=dep[pre]+1,fa[u]=pre,sz[u]=1;
for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
int v=edge::e[i].v;
if(v==pre) continue;
init(v,u),sz[u]+=sz[v];
if(sz[v]>mxson) mxson=sz[v],son[u]=v;
}
return;
}
int top[N],dfn[N],cnt,wi[N];
il void getnum(int u,int tp){
top[u]=tp,dfn[u]=++cnt,wi[cnt]=w[u];
if(!son[u]) return;
getnum(son[u],tp);
for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
int v=edge::e[i].v;
if(v==fa[u]||v==son[u]) continue;
getnum(v,v);
}
return;
}
} using namespace qwq;
namespace tree{ //线段树
#define ls (rt<<1)
#define rs ((rt<<1)|1)
#define mid ((l+r)>>1)
int tr[N<<2],lz[N<<2];
il void pushup(int rt){
return tr[rt]=(tr[ls]+tr[rs])%mod,void();
}
il void pushdown(int rt,int l,int r){
if(!lz[rt]) return;
lz[ls]=(lz[ls]+lz[rt])%mod;
lz[rs]=(lz[rs]+lz[rt])%mod;
tr[ls]=(tr[ls]+(lz[rt]*(mid-l+1))%mod)%mod;
tr[rs]=(tr[rs]+(lz[rt]*(r-mid))%mod)%mod;
return lz[rt]=0,void();
}
il void build(int rt,int l,int r){
if(l==r) return tr[rt]=wi[l],void();
build(ls,l,mid),build(rs,mid+1,r);
return pushup(rt);
}
il void add(int rt,int l,int r,int ql,int qr,int k){
if(ql<=l&&r<=qr){
tr[rt]=(tr[rt]+(r-l+1)*k%mod)%mod;
return lz[rt]=(lz[rt]+k)%mod,void();
} pushdown(rt,l,r);
if(mid>=ql) add(ls,l,mid,ql,qr,k);
if(mid<qr) add(rs,mid+1,r,ql,qr,k);
return pushup(rt);
}
il int ask(int rt,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return tr[rt];
pushdown(rt,l,r);int as=0;
if(mid>=ql) as=(as+ask(ls,l,mid,ql,qr))%mod;
if(mid<qr) as=(as+ask(rs,mid+1,r,ql,qr))%mod;
return as;
}
#undef ls
#undef rs
#undef mid
} using namespace tree;
namespace operate{
il void op1(int x,int y,int z){ //将树从x到y结点最短路径上所有节点的值都加上z。
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
add(1,1,n,dfn[top[x]],dfn[x],z),x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return add(1,1,n,dfn[x],dfn[y],z);
}
il int op2(int x,int y){ //求树从x到y结点最短路径上所有节点的值之和
int as=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
as=(as+ask(1,1,n,dfn[top[x]],dfn[x]))%mod;
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return as=(as+ask(1,1,n,dfn[x],dfn[y]))%mod;
}
il void op3(int x,int z){ //将以x为根节点的子树内所有节点值都加上z
return add(1,1,n,dfn[x],dfn[x]+sz[x]-1,z);
}
il int op4(int x){ //求以x为根节点的子树内所有节点值之和
return ask(1,1,n,dfn[x],dfn[x]+sz[x]-1);
}
} using namespace operate;
int main(){
cin>>n>>m>>root>>mod;
for(ri int i=1;i<=n;++i) cin>>w[i],w[i]%=mod;
for(ri int i=1,u,v;i<n;++i) cin>>u>>v,edge::add(u,v,i);
init(root,root),getnum(root,root),build(1,1,n);
for(ri int i=1,op,x,y,z;i<=m;++i){
cin>>op>>x;
if(op==1) cin>>y>>z,z%=mod,op1(x,y,z);
if(op==2) cin>>y,cout<<op2(x,y)<<'\n';
if(op==3) cin>>z,z%=mod,op3(x,z);
if(op==4) cout<<op4(x)<<'\n';
}
return 0;
}
长链剖分
- 长链剖分本质上就是另外一种链剖分方式。
- 长链剖分的剖分方法与轻重链剖分极其相似,只需要把以子树大小判断重儿子改成以节点深度判断即可
- 长链剖分从一个节点到根的路径的轻边切换条数是 \(\sqrt{n}\) 级别的
- 一般情况下可以使用长链剖分来优化的 DP 会有一维状态为深度维。
to be continue~