树链剖分
板子:P3384
查错查的心态都炸了
前言
链剖分:指对数的边进行划分的一类操作,目的是减少在链上修改、查询等操作的复杂度
树链剖分有三类:轻重链剖分、虚实剖分和长链剖分
而今天我要讲的是轻重链剖分
思想
通过轻重链剖分将树分成多条链,保证每个节点都只属于一条链
\(\color{#66ccff} {1重儿子}\)
节点\(x\)的子节点中,包含子节点数目最多的子节点为该节点的重儿子
\(\color{#39c5bb} {2轻儿子}\)
节点\(x\)的字节点中,除了重儿子,其余都是轻儿子
\(\color{#66ccff} {3重链}\)
节点到重儿子之间的路径为重链,每条重链相当于一个区间,把所有重链首尾相接组成一个线性节点序列,再通过数据结构维护即可(相邻重边连起来的 连接一条重儿子 的链叫重链)
\(\color{#39c5bb} {4轻链}\)
同上
\(\color{#66ccff} {5重边}\)
连接该节点与它的重儿子的边
\(\color{#39c5bb} {6轻边}\)
同上
例:
蓝色为重儿子,红色为重边———图转自作者
前置
#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1)
#define N 200010
using namespace std;
int n,m,r,p;
int tot,first[N],nex[N],to[N],w[N],wt[N];//w[]、wt[]初始点权数组+邻接表
int a[N<<2],laz[N<<2];//线段树+懒标
int son[N],id[N],fa[N],cnt,dep[N],siz[N],top[N];//son[]重儿子编号,id[]新编号,fa[]父亲节点,cnt dfs_clock/dfs序,dep[]深度,siz[]子树大小,top[]当前链顶端节点
int res=0;//查询答案
预处理
注意:先处理重儿子,再处理轻儿子!
因为顺序是先重再轻,所以每一条重链的新编号是连续的
因为是dfs,所以每一个子树的新编号也是连续的
完成重链与轻链的处理
分别是\(\color{red}{df1}和\color{green} {dfs2}\)
\(\huge {dfs1:}\)
inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度
dep[x]=deep;
fa[x]=f;
siz[x]=1;
int maxson=-1;
for(int i=first[x];i;i=nex[i]){
int y=to[i];
if(y==f) continue;
dfs1(y,x,deep+1);
siz[x]+=siz[y];
if(siz[y]>maxson) son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号
}
}
\(\huge {dfs2}\):
inline void dfs2(int x,int topf){////x当前节点,topf当前链的最顶端的节点
id[x]=++cnt;//标记每个点的新编号
wt[cnt]=w[x];//把每个点的初始值赋到新编号上来
top[x]=topf;
if(!son[x]) return ;//先处理重儿子,在处理轻儿子--因为根据设置的编号要使得整棵树最后连续
dfs2(son[x],topf);
for(int i=first[x];i;i=nex[i]){
int y=to[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
}
然后建立线段树
//======================线段树======================
inline void pushdown(int rt,int lenn){
laz[rt<<1]+=laz[rt];
laz[rt<<1|1]+=laz[rt];
a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
a[rt<<1|1]+=laz[rt]*(lenn>>1);
a[rt<<1]%=p;
a[rt<<1|1]%=p;
laz[rt]=0;
}
inline void build(int rt,int l,int r){
if(l==r){
a[rt]=wt[l];
if(a[rt]>p) a[rt]%=p;
return ;
}
build(lson);
build(rson);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
inline void update(int rt,int l,int r,int L,int R,int z){
if(L<=l&&r<=R){
laz[rt]+=z;
a[rt]+=z*len;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) update(lson,L,R,z);
if(R>mid) update(rson,L,R,z);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
}
inline void query(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){
res+=a[rt];
res%=p;
return ;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) query(lson,L,R);
if(R>mid) query(rson,L,R);
}
}
不同的操作
1:区间更新
inline void updRange(int x,int y,int z){
z%=p;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],z);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],z);
}
1:如果\(u\)和\(v\)在同一条重链上,则在线段树上查询其对应的下标区间id[u]到id[v]
2:反之,则一边查询,一边将u和v向同一条重链上移,再处理
2区间查询
inline int qRange(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=p;
x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
}//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//保证x的深度更大
res=0;
query(1,1,n,id[x],id[y]);
ans+=res;
return ans%p;
}
3单点更新和修改
区间id[x]到id[x]+siz[x]-1全部都要更新和查询
inline void updSon(int x,int y){
update(1,1,n,id[x],id[x]+siz[x]-1,y);
}
inline int qSon(int x){
res=0;
query(1,1,n,id[x],id[x]+siz[x]-1);
return res;
}
线段树
//======================线段树======================
inline void pushdown(int rt,int lenn){
laz[rt<<1]+=laz[rt];
laz[rt<<1|1]+=laz[rt];
a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
a[rt<<1|1]+=laz[rt]*(lenn>>1);
a[rt<<1]%=p;
a[rt<<1|1]%=p;
laz[rt]=0;
}
inline void build(int rt,int l,int r){
if(l==r){
a[rt]=wt[l];
if(a[rt]>p) a[rt]%=p;
return ;
}
build(lson);
build(rson);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
inline void update(int rt,int l,int r,int L,int R,int z){
if(L<=l&&r<=R){
laz[rt]+=z;
a[rt]+=z*len;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) update(lson,L,R,z);
if(R>mid) update(rson,L,R,z);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
}
inline void query(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){
res+=a[rt];
res%=p;
return ;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) query(lson,L,R);
if(R>mid) query(rson,L,R);
}
}
总代码:
#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1)
#define N 200010
using namespace std;
int n,m,r,p;
int tot,first[N],nex[N],to[N],w[N],wt[N];//w[]、wt[]初始点权数组
int a[N<<2],laz[N<<2];
int son[N],id[N],fa[N],cnt,dep[N],siz[N],top[N];//son[]重儿子编号,id[]新编号,fa[]父亲节点,cnt dfs_clock/dfs序,dep[]深度,siz[]子树大小,top[]当前链顶端节点
int res=0;//查询答案
//======================快读========================
inline int read(){
int p=0,f=1;
char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){p=p*10+c-'0';c=getchar();}
return p*f;
}
//====================邻接表========================
inline void add(int x,int y){//链式前向星加边
to[++tot]=y;
nex[tot]=first[x];
first[x]=tot;
}
//===================初始化=========================
inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度
dep[x]=deep;
fa[x]=f;
siz[x]=1;
int maxson=-1;
for(int i=first[x];i;i=nex[i]){
int y=to[i];
if(y==f) continue;
dfs1(y,x,deep+1);
siz[x]+=siz[y];
if(siz[y]>maxson) son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号
}
}
inline void dfs2(int x,int topf){////x当前节点,topf当前链的最顶端的节点
id[x]=++cnt;//标记每个点的新编号
wt[cnt]=w[x];//把每个点的初始值赋到新编号上来
top[x]=topf;
if(!son[x]) return ;
dfs2(son[x],topf);
for(int i=first[x];i;i=nex[i]){
int y=to[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
}
//======================线段树======================
inline void pushdown(int rt,int lenn){
laz[rt<<1]+=laz[rt];
laz[rt<<1|1]+=laz[rt];
a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
a[rt<<1|1]+=laz[rt]*(lenn>>1);
a[rt<<1]%=p;
a[rt<<1|1]%=p;
laz[rt]=0;
}
inline void build(int rt,int l,int r){
if(l==r){
a[rt]=wt[l];
if(a[rt]>p) a[rt]%=p;
return ;
}
build(lson);
build(rson);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
inline void update(int rt,int l,int r,int L,int R,int z){
if(L<=l&&r<=R){
laz[rt]+=z;
a[rt]+=z*len;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) update(lson,L,R,z);
if(R>mid) update(rson,L,R,z);
a[rt]=(a[rt<<1]+a[rt<<1|1])%p;
}
}
inline void query(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){
res+=a[rt];
res%=p;
return ;
}
else{
if(laz[rt]) pushdown(rt,len);
if(L<=mid) query(lson,L,R);
if(R>mid) query(rson,L,R);
}
}
//======================操作========================
inline void updRange(int x,int y,int z){
z%=p;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],z);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],z);
}
inline int qRange(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=p;
x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
}//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//保证x的深度更大
res=0;
query(1,1,n,id[x],id[y]);
ans+=res;
return ans%p;
}
inline void updSon(int x,int y){
update(1,1,n,id[x],id[x]+siz[x]-1,y);
}
inline int qSon(int x){
res=0;
query(1,1,n,id[x],id[x]+siz[x]-1);
return res;
}
//======================主函数======================
int main(){
n=read(),m=read(),r=read(),p=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++){
int a,b;
a=read();b=read();
add(a,b);add(b,a);
}
dfs1(r,0,1);
dfs2(r,r);
build(1,1,n);
while(m--){
int k,x,y,z;
k=read();
if(k==1){
x=read(),y=read(),z=read();
updRange(x,y,z);
}
else if(k==2){
x=read(),y=read();
cout<<qRange(x,y)<<endl;
}
else if(k==3){
x=read(),y=read();
updSon(x,y);
}
else{
x=read();
cout<<qSon(x)<<endl;
}
}
return 0;
}
总结
通过对树进行划分,得到一个新的区间,由线段树进行维护,可以降低操作的时间复杂度