轻重链剖分
树链剖分,一个让你代码量翻倍的能优雅解决树上问题的神奇方式。
树剖就是把树拆成链,拆成序列,然后就可以用序列方式处理这棵树。
一道树剖题Luogu 3384
题目大概意思:给一有根树,给定四种操作
- 树上两点,及他俩路径上点权+\(d\)
- 查询树上两点及路径上点权和
- 某点及子树+\(d\)
- 查询某点及子树求和
对于2 4操作,查询的结果对给出的数取模并输出。
于是就开始了愉快的树链剖分,两次dfs将树拆成链,怎么拆?我们定义一个点,它的重儿子是子树\(size\)最大的一个儿子,然后一直沿着重儿子走就是一条重链。除了重儿子剩下的都是轻儿子。
void Dfs(int x,int fa){
f[x]=fa;dep[x]=dep[fa]+1;si[x]=1;//初始化父亲,深度,子树大小
int ms=-1;//儿子中子树最大值初始化
for(int p=h[x];p;p=e[p].nxt){
int to=e[p].to;//遍历所有点 前向星存边
if(to==fa)continue;//是父亲就别访问了,从父亲来的,不必反复横跳
Dfs(to,x);//处理儿子
si[x]+=si[to];//自己的子树大小=初始的自己+第一个儿子子树+第二个儿子.... 所以一直加就好了
if(si[to]>ms){//如果儿子大于目前最大的儿子大小
ms=si[to];//新的最大值
wson[x]=to;//新的重儿子
}
}
}
第一遍dfs得到一些信息,探探路,把重儿子是谁什么的摸透了,再来拆重儿子链。
dfs(rt,rt);//两个要素 第一个是当前点 第二个是目前链的顶点,所以一开始要传根节点和根节点
void dfs(int x,int topf){
top[x]=topf;//你被访问到,如果你是轻儿子,那么topf进来的时候会改成你,链顶是你,否则你是重儿子,你在重链上,topf就是父亲传下来的顶端
id[x]=++tot;//时间戳,这是第几个结点
w[tot]=a[x];//变成新的序列之后,新的序列第tot个点的权值是x的权值
if(!wson[x])return;//没有重儿子->没有儿子->叶子节点->返回,如果不返回可能会叶子->0->0->0...套娃
dfs(wson[x],topf);//访问重儿子,重儿子在重链上,传topf
for(int p=h[x];p;p=e[p].nxt){
int to=e[p].to;//遍历
if(to==f[x])continue;//父亲不去
if(to==wson[x])continue;//遍历过不去
dfs(to,to);//这里要改成to,因为除了重儿子外都是轻儿子,如果是轻儿子,topf就要改成它自己
}
}
轻儿子一定是一个重链的链头,因为它的兄弟是重儿子,就说明父亲的重链不在自己这里,所以自己要重新开一个重链,走自己的重儿子让别人说去吧,所以轻儿子遍历传to to,相当是新的链
于是就剖完了,对于原来树上点 \(id[x]\)是它第几个被访问到,就是他在 树 转化成的 序列 里的下标[位置 \(position\)],\(w\)数组就是序列中点的值,是将树变为序列之后,每一个点的值。\(w[id[x]]==a[x]\)。以后树上的处理就在序列里处理就行了,已经用序列存下了树所有点的值,也知道树上每一个点在序列里的位置。
注意一个非常神奇的地方,如果你要修改查询一个子树,这个子树一定是在序列里连续的,为什么?因为dfs是深度优先,所以一定会先访问完这个点所有的值,所以如果要修改一个点\(x\),因为是连续的,其实就等同于修改查询区间\(w[id_x,id_x+size_x-1]\),\(x\)在序列中的位置加上子树大小-1就是\(x\)最后一个结点的位置,所以对这个区间操作,就是对x子树操作。
void subtreemodify(int x,int z){
modify(1,1,n,id[x],id[x]+si[x]-1,z);//子树加
return;
}
LL subtreequery(int x){
return query(1,1,n,id[x],id[x]+si[x]-1);//子树和
}
然后区间加和区间和就用线段树就可以了,注意线段树不要去维护一开始输入的\(a\)数组,而要维护转化为序列的\(w\)数组,这个序列中x和其子树才是连续的一段,可以用\(id\)来搞。
总结
- 对于子树修改
1. 因为x及子树是序列中连续的区间,所以找到x的位置,线段树将区间直接修改 - 对于子树查询
1. 因为连续,线段树直接查询
然后就是令人绝望的两点间修改查询。思想就是一条链一条链改,比如两个点,我先看看是不是一个链,不是一个链,深度比较深的点优先往上跳。
先对这个点直到链首查询或修改,为什么可以这么做?因为我们优先走重儿子,所以链首到链上一个点必然也是序列中连续的区间,所以依然可以线段树\(query\)和\(modify\),然后这个点再跳到链首的父亲[跳到链首可能就一直自己跳自己死循环]。
直到一个链之后,我们用线段树直接查询两点间,或者修改两点间[用\(id\)来修改查询]。
总结 修改和查询统称为操作[先总结再上代码
- 查询是否一个链
- 如果不是一个链
1. 比较链头深度
2. 链头深的点,因为链上点在区间连续,所以直接操作这个点到其链头
3. 链头深的点跳到链头的父亲
4. 返回查询是否一个链操作 - 如果是一个链
1. 修改两点间[\(x.id\)~\(y.id\)]
2. 所有操作均已结束,结束操作。
是不是忘了什么
- 如果不是一个链
void chainmodify(int x,int y,int z){
z%=md;//取模
while(top[x]!=top[y]){//不是一个链[chain]
if(dep[top[x]]<dep[top[y]])swap(x,y);//取深度深的[大的
modify(1,1,n,id[top[x]],id[x],z);//操作
x=f[top[x]];//跳
}
if(dep[x]>dep[y])swap(x,y);//同一个链上了 取深度小的[看个人喜好?]当x,因为深度小id小,习惯写的时候先x后y
modify(1,1,n,id[x],id[y],z);//操作
return;//结束
}
LL chainquery(int x,int y){//同理
LL re=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
re+=query(1,1,n,id[top[x]],id[x]);
x=f[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
re+=query(1,1,n,id[x],id[y]);
return re;
}
完整代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+8;
typedef long long LL;
LL md;
LL a[N],w[N];
int top[N],tot,id[N];
int n,m,rt;
int h[N],cnt=1;
struct edg{
int to,nxt;
}e[N<<1];
int f[N],dep[N],wson[N],si[N];
struct Node{
LL sum,lz;
}tre[N<<2];
void down(int x,int l,int r){
//puts("-------");
//system("pause");
int mid=(l+r)/2,ls=x*2,rs=x*2+1;
tre[ls].sum+=(mid-l+1)*tre[x].lz;
tre[ls].sum%=md;
tre[rs].sum+=(r-mid)*tre[x].lz;
tre[rs].sum%=md;
tre[ls].lz=tre[ls].lz+tre[x].lz;
tre[rs].lz=tre[rs].lz+tre[x].lz;
tre[x].lz=0;
return;
}
void modify(int x,int l,int r,int L,int R,int d){
int mid=(l+r)/2,ls=x*2,rs=x*2+1;
if(l>=L && r<=R){
tre[x].sum+=(r-l+1)*d;
tre[x].sum%=md;
tre[x].lz+=d;
return;
}
down(x,l,r);
if(mid>=L){
modify(ls,l,mid,L,R,d);
}
if(mid+1<=R){
modify(rs,mid+1,r,L,R,d);
}
tre[x].sum=(tre[ls].sum+tre[rs].sum)%md;
return;
}
LL query(int x,int l,int r,int L,int R){
int mid=(l+r)/2,ls=x*2,rs=x*2+1;
if(L<=l && r<=R){
return tre[x].sum;
}
down(x,l,r);
LL ans=0;
if(L<=mid){
ans+=query(ls,l,mid,L,R);
ans%=md;
}
if(mid+1<=R){
ans+=query(rs,mid+1,r,L,R);
ans%=md;
}
return ans;
}
void Build(int x,int l,int r){
int mid=(l+r)/2,ls=x*2,rs=x*2+1;
if(l==r){
tre[x].sum=w[l]%md;//l其实是id 就是tot
}
else{
Build(ls,l,mid);
Build(rs,mid+1,r);
tre[x].sum=(tre[ls].sum+tre[rs].sum)%md;
}
}
void chainmodify(int x,int y,int z){
z%=md;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);//深度小的高
modify(1,1,n,id[top[x]],id[x],z);
x=f[top[x]];
}
if(dep[x]>dep[y])swap(x,y);//同一个链上了 深度大的id大
modify(1,1,n,id[x],id[y],z);
return;
}
LL chainquery(int x,int y){
LL re=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
re+=query(1,1,n,id[top[x]],id[x]);
x=f[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
re+=query(1,1,n,id[x],id[y]);
return re;
}
void subtreemodify(int x,int z){
modify(1,1,n,id[x],id[x]+si[x]-1,z);
return;
}
LL subtreequery(int x){
return query(1,1,n,id[x],id[x]+si[x]-1);
}
void Dfs(int x,int fa){
f[x]=fa;dep[x]=dep[fa]+1;si[x]=1;
int ms=-1;
for(int p=h[x];p;p=e[p].nxt){
int to=e[p].to;
if(to==fa)continue;
Dfs(to,x);
si[x]+=si[to];
if(si[to]>ms){//如果儿子大于目前最大的儿子大小
ms=si[to];//新最大
wson[x]=to;//新重儿子
}
}
}
void dfs(int x,int topf){
top[x]=topf;//链顶端结点
id[x]=++tot;//时间戳
w[tot]=a[x];//tot个点的权值是x的权值
if(!wson[x])return;//没有重儿子->没有儿子->叶子节点->返回
dfs(wson[x],topf);
for(int p=h[x];p;p=e[p].nxt){
int to=e[p].to;
if(to==f[x])continue;
if(to==wson[x])continue;
dfs(to,to);
}
}
void add(int u,int v){
e[++cnt]=(edg){v,h[u]};
h[u]=cnt;
}
int main(){
scanf("%d%d%d%lld",&n,&m,&rt,&md);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int u,v,i=1;i<n;i++){
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
Dfs(rt,0);
dfs(rt,rt);
Build(1,1,n);
for(int i=1;i<=m;i++){
int opt,x,y,z;
scanf("%d",&opt);
if(opt==1){
scanf("%d%d%d",&x,&y,&z);
chainmodify(x,y,z);
}
if(opt==2){
scanf("%d%d",&x,&y);
LL ans=chainquery(x,y);
ans%=md;
printf("%lld\n",ans);
}
if(opt==3){
scanf("%d%d",&x,&z);
subtreemodify(x,z);
}
if(opt==4){
scanf("%d",&x);
LL ans=subtreequery(x);
ans%=md;
printf("%lld\n",ans);
}
}
return 0;
}