树链剖分入门
Posted lcguo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树链剖分入门相关的知识,希望对你有一定的参考价值。
又是一个美妙的算法
定义
- 重儿子:在一个点 (x) 的子节点中,拥有最大子树的儿子 (y) 是 (x) 的重儿子。
- 轻儿子:在一个点 (x) 的子节点中,除去重儿子的子节点为 (x) 的轻儿子。
- 重边:父亲与重儿子的连边。
- 轻边:父亲与轻儿子的连边。
- 重链:只由重边构成的链。
我们看上面这张图,实线是重边,虚线是轻边
性质
用 (size_i) 表示以 (i) 为根的子树的大小
性质一:如果 (v) 是 (u) 的轻儿子,那么 (size_vleqdfrac{size_u}{2})
反证法:如果 (size_v>dfrac{size_u}{2}) 即轻儿子的大小超过了 (size_u) 的一半,那么 (u) 的其他儿子的子树大小一定小于 (size_u) 的一半,因此点 (v) 一定是重儿子,与假设不符。
性质二:任意点 (u) 到根的路径上轻边、重链条数都不大于 (log_2!n)。
证明:
考虑从根((size_{root}=n))开始往 (u) 走,每经过一条轻边,根据性质一,(size) 都至少会减小一半。
而走到 (u) 时 (size_uge 1),因此经过的轻边条数必定 (leq log_2!n)。
重链与重链之间是被轻边隔开的,因此经过的重链的条数与轻边的条数之差的绝对值不超过1。
性质三:一个点的子树内节点的 dfs 序是连续的
是不是一目了然了
模板题
我们先来接触一道模板题——洛谷P3384
【题目描述】
如题,已知一棵包含 (N) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作 (1): 格式: (1 x y z) 表示将树从 (x) 到 (y) 结点最短路径上所有节点的值都加上 (z)。
操作 (2): 格式: (2 x y) 表示求树从 (x) 到 (y) 结点最短路径上所有节点的值之和。
操作 (3): 格式: (3 x z) 表示将以 (x) 为根节点的子树内所有节点值都加上 (z)。
操作 (4): 格式: (4 x) 表示求以 (x) 为根节点的子树内所有节点值之和
【输入格式】
第一行包含 (4) 个正整数 (N,M,R,P),分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。
接下来一行包含 (N) 个非负整数,分别依次表示各个节点上初始的数值。
接下来 (N-1) 行每行包含两个整数 (x,y),表示点 (x) 和点 (y) 之间连有一条边(保证无环且连通)。
接下来 (M) 行每行包含若干个正整数,每行表示一个操作,格式如下:
操作 (1): (1 x y z);
操作 (2): (2 x y);
操作 (3): (3 x z);
操作 (4): (4 x)。
【输出格式】
输出包含若干行,分别依次表示每个操作 (2) 或操作 (4) 所得的结果(对 (P) 取模)
【样例输入】
5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3
【样例输出】
2
21
【数据规模与约定】
对于 (30\%) 的数据: (1 leq N leq 10,1 leq M leq 10);
对于 (70\%) 的数据: (1 leq N leq {10}^3, 1 leq M leq {10}^3);
对于 (100\%) 的数据: (1le N leq {10}^5, 1le M leq {10}^5,1le Rle N,1le P le 2^{31}-1)。
我们先考虑操作 (3) 和 (4):
因为一个点的子树内节点的 dfs 序是连续的,我们就可以将一个点的 dfs 序看作它的位置
一个点的子树看作是一个 dfs 序的区间,子树加的操作相当于是在某个 dfs 序的区间内加上一个值,询问相当于是查询 dfs 序的某一个区间上的权值和。
问题转化为了区间加、区间查询和。
线段树可以在 (O(m log_2!n)) 的时间内完成。
再考虑操作 (1) 和 (2):
我们可以用树链剖分来处理,在求 dfs 序时,我们按照优先重??的顺序求出每个点的 dfs 序,此时?条重链上的节点的 dfs 序是连续的。
(u) 到 (v) 的路径可以拆分成两条只向上的链。
设 (t=operatorname{LCA}(u,v)),那么为 (u) 到 (v) 的路径上每个点权值 (+x) 相当于为 (u) 到 (t)、(v) 到 (t) 路径上的节点 (+x),而点 (t) 被
加了两次,减掉即可,查询的时候同理。
接下来我们只需要考虑一条自下而上的链 ((x, y))。 根据树链剖分的性质,这条路径是由不超过 (log_2!n) 条重链的一部分构成的。
当目前的 (x) 与 (y) 处在不同的重链中时,意味着 (x) 需要跳过所在的重链,因此可以对这条重链从起点到 (x) 的 dfs 序区间进行操作,并跳到这条重链起点的父亲处。
当目前的 (x) 与 (y) 处在相同的重链中时,只需要将介于 (y) 与 (x) 之间的节点进行操作即可结束过程。
每次的操作都是一个线段树上的区间加、询问区间和的过程,单次 (O(log_2!n)),而由于一次最多在 (O(log_2! n))条重链上进行操作,因此总复杂度 (O(mlog_2^2!n))。
下面是代码讲解时间:
(fa[i]) 表示 (i) 的父亲节点,(dep[i]) 表示 (i) 的深度,(son[i]) 表示 (i) 的重儿子,(Size[i]) 表示 (i) 的子树大小;
(id[i]) 表示 (i) 的 dfs 序,也就是点 (i) 在线段树中的编号;(bi[i]) 表示 dfs 序为 (i) 的点的序号
(top[i]) 表示 (i) 所在重链的起点
首先,我们可以用一遍 dfs 求出 (fa,Size,dep,son) 数组,这个应该没什么好讲的
void dfs_first(int x,int _fa){
Size[x]=1;
for(rint i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==_fa) continue;
fa[y]=x;
dep[y]=dep[x]+1;
dfs_first(y,x);
Size[x]+=Size[y];
if(Size[y]>Size[son[x]]) son[x]=y;
}
}
当求出每个点的重儿子后,我们就可以再通过一次 dfs 来求出 (id,top,bi) 数组
void dfs_second(int x,int top_point){
id[x]=++cnt; top[x]=top_point; bi[cnt]=x;
if(!son[x]) return;//叶子节点直接退出
dfs_second(son[x],top_point);//重儿子的链顶不变
for(rint i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs_second(y,y);//轻儿子是另一条重链的顶部
}
}
考虑如何实现区间加法,类似于求 lca 的过程,在找 lca 的过程中将经过的区间统统 (+v)
void op_add(int x,int y,int v){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
change(1,1,n,id[top[x]],id[x],v);//将x到重链顶端这个区间+v
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
change(1,1,n,id[y],id[x],v);
}
区间求和可以举一反三
然后美妙的线段树我就不解释了
整个的代码如下:
#include<bits/stdc++.h>
#define rint register int
using namespace std;
inline int read(){
int s=0,f=1; char c=getchar();
while(c<‘0‘||c>‘9‘){if(c==‘-‘)f=0;c=getchar();}
while(c>=‘0‘&&c<=‘9‘) s=(s<<1)+(s<<3)+(c^48),c=getchar();
return f?s:-s;
}
int n,m,root,Mod,val[100010];
int tot,head[100010],ver[200010],nxt[200010];
int Size[100010],son[100010],fa[100010],dep[100010];
int top[100010],id[100010],cnt,bi[100010];
void add(int x,int y){
nxt[++tot]=head[x]; ver[tot]=y;
head[x]=tot;
}
void dfs_first(int x,int _fa){
Size[x]=1;
for(rint i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==_fa) continue;
fa[y]=x; dep[y]=dep[x]+1;
dfs_first(y,x);
Size[x]+=Size[y];
if(Size[y]>Size[son[x]]) son[x]=y;
}
}
void dfs_second(int x,int top_point){
id[x]=++cnt; top[x]=top_point; bi[cnt]=x;
if(!son[x]) return;
dfs_second(son[x],top_point);
for(rint i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs_second(y,y);
}
}
//下面是线段树板子
int Sum[400010],Add[400010];
void spread(int p,int l,int r){
int mid=l+r>>1,lp=p<<1,rp=p<<1|1;
if(Add[p]){
Sum[lp]=1ll*(Sum[lp]+1ll*Add[p]*(mid-l+1)%Mod)%Mod;
Sum[rp]=1ll*(Sum[rp]+1ll*Add[p]*(r-mid)%Mod)%Mod;
Add[lp]=1ll*(Add[lp]+Add[p])%Mod;
Add[rp]=1ll*(Add[rp]+Add[p])%Mod;
Add[p]=0;
}
return;
}
void build(int p,int l,int r){
if(l==r) return Sum[p]=val[bi[l]],void();
int mid=l+r>>1,lp=p<<1,rp=p<<1|1;
build(lp,l,mid); build(rp,mid+1,r);
Sum[p]=Sum[lp]+Sum[rp];
}
void change(int p,int l,int r,int x,int y,int v){
if(l>=x&&r<=y){
Sum[p]=1ll*(Sum[p]+1ll*v*(r-l+1)%Mod)%Mod;
Add[p]=1ll*(Add[p]+v)%Mod;
return;
}
spread(p,l,r);
int mid=l+r>>1,lp=p<<1,rp=p<<1|1;
if(x<=mid) change(lp,l,mid,x,y,v);
if(y>mid) change(rp,mid+1,r,x,y,v);
Sum[p]=Sum[lp]+Sum[rp];
}
int ask_Sum(int p,int l,int r,int x,int y){
if(l>=x&&r<=y) return Sum[p];
spread(p,l,r);
int mid=l+r>>1,lp=p<<1,rp=p<<1|1,val=0;
if(x<=mid) val=1ll*(val+ask_Sum(lp,l,mid,x,y))%Mod;
if(y>mid) val=1ll*(val+ask_Sum(rp,mid+1,r,x,y))%Mod;
return val;
}
//上面是线段树板子
void op_add(int x,int y,int v){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
change(1,1,n,id[top[x]],id[x],v);
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
change(1,1,n,id[y],id[x],v);
}
int op_sum(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=1ll*(ans+ask_Sum(1,1,n,id[top[x]],id[x]))%Mod;
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
ans=1ll*(ans+ask_Sum(1,1,n,id[y],id[x]))%Mod;
return ans;
}
int main(){
n=read(); m=read(); root=read(); Mod=read();
for(rint i=1;i<=n;++i) val[i]=read();
for(rint i=1;i<n;++i){
int x=read(),y=read();
add(x,y); add(y,x);
}
dfs_first(root,0); dfs_second(root,root); build(1,1,n);
while(m--){
int type=read();
if(type==1){
int x=read(),y=read(),z=read();
op_add(x,y,z);
}
if(type==2){
int x=read(),y=read();
printf("%d
",op_sum(x,y));
}
if(type==3){
int x=read(),y=read();
change(1,1,n,id[x],id[x]+Size[x]-1,y);
}
if(type==4){
int x=read();
printf("%d
",ask_Sum(1,1,n,id[x],id[x]+Size[x]-1));
}
}
return 0;
}
我就算打暴力,n 方修改,n方查询,也不会写树剖这种码量的模板题
真香~~
以上是关于树链剖分入门的主要内容,如果未能解决你的问题,请参考以下文章