0.序
是一道模板题,写这篇题解是为了骗取阅读量把我对树剖的见解写出来+巩固。
1.概述
树链剖分是一种毒瘤算法,她可以用来解决一些树上毒瘤问题。
有这样一群毒瘤出题人,他们把序列上很简单的问题搬到了树上,于是就有了树链剖分。
基本思路是将整棵树剖分成轻重链,然后用毒瘤数据结构分别维护,所以树链剖分\(\approx\)优雅的暴力。
先给出几个定义:
重儿子:一个节点子节点最多的儿子。
轻儿子:除了重儿子之外的儿子。
重边:父亲连向重儿子的边。
轻边:不是重边的边。
重链:重边连起来的链。特别地,两端均连轻边的节点自成一条重链。
轻链:非重链的路径。所有轻链的长度都是1。
还有几个数组:
hvs[]:当前节点的重儿子。
ltp[]:当前节点所在重链的顶端(深度最小)。
两次DFS:
第一次:求fa、dep、siz等基本数组和hvs。hvs可以直接DFS每个节点比较求出,相信学到树剖的大佬已经会了,不多讲。
第二次:求ltp数组。如果当前节点是重儿子,直接继承当前重链头;否则以当前节点为重链头,继续DFS。
DFS完之后,我们可爱的小树已经被拍扁成DFS序列了,接下来只需用线段树/树状数组等毒瘤数据结构来维护即可。
给出几个例子:
1.更改/查询x到y的路径
只需按照求LCA的步骤,依次上跳。注意树剖的上跳是u=fa[ltp[u]]
,也就是一次跳一条重链+轻链,可以思考一下她的复杂度。
因为DFS序里一条链上的编号都是连续的,所以直接线段树修改/查询。
给出修改的例子:
while(ltp[u]!=ltp[v]){
if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
Modify(1,idx[ltp[u]],idx[u],add);
u=fa[ltp[u]];
}
if(dep[u]>dep[v])swap(u,v);
Modify(1,idx[u],idx[v],add);
2.更改/查询x的子树
容易发现,一棵子树在DFS序列里也是连续的,所以直接维护即可。
给出修改的例子:
Modify(1,idx[u],idx[u]+siz[u]-1,add);
具体实现细节可以参考代码。
2.代码
#define N 100010
int n,m,r,p,tot;
int siz[N],fa[N],dep[N],ipt[N];
int hvs[N],ltp[N],idx[N],num[N];
struct Edge {
int to,nxt;
}e[N<<1];
int head[N],cnt;
inline void ade(int u,int v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
struct Node {
int l,r,tag,wei;
}tr[N<<2];
void DFS1(int now,int ff){
fa[now]=ff;
siz[now]=1;
dep[now]=dep[ff]+1;
for(rg int i=head[now];i;i=e[i].nxt){
int v=e[i].to;
if(v!=ff){
DFS1(v,now);
siz[now]+=siz[v];
if(siz[v]>siz[hvs[now]])hvs[now]=v;
}
}
}
void DFS2(int now,int tp){
ltp[now]=tp;
idx[now]=++tot;
num[tot]=ipt[now];
if(hvs[now])DFS2(hvs[now],tp);
for(rg int i=head[now];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa[now]){
if(v!=hvs[now])DFS2(v,v);
}
}
}
void Build(int k,int l,int r){
tr[k].l=l,tr[k].r=r;
if(tr[k].l==tr[k].r){
tr[k].wei=num[l]%p;
return;
}
Build(ls,l,nmid);
Build(rs,nmid+1,r);
pushup;
}
inline void Pushdn(int k){
if(tr[k].tag){
tr[ls].wei+=tr[k].tag*(tr[ls].r-tr[ls].l+1)%p;
tr[rs].wei+=tr[k].tag*(tr[rs].r-tr[rs].l+1)%p;
tr[ls].wei%=p,tr[rs].wei%=p;
tr[ls].tag+=tr[k].tag%p;
tr[rs].tag+=tr[k].tag%p;
tr[ls].tag%=p,tr[rs].tag%=p;
tr[k].tag=0;
}
}
void Modify(int k,int l,int r,int add){
if(l<=tr[k].l&&tr[k].r<=r){
tr[k].wei+=add%p*(tr[k].r-tr[k].l+1)%p;
tr[k].tag+=add%p;
tr[k].wei%=p,tr[k].tag%=p;
return;
}
Pushdn(k);
if(l<=tmid)Modify(ls,l,r,add);
if(tmid<r)Modify(rs,l,r,add);
pushup;
}
int Query(int k,int l,int r){
if(l<=tr[k].l&&tr[k].r<=r){
return tr[k].wei%p;
}
Pushdn(k);
int res=0;
if(l<=tmid)res=(res+Query(ls,l,r))%p;
if(tmid<r)res=(res+Query(rs,l,r))%p;
return res;
}
void Opt1(int u,int v,int add){
add%=p;
while(ltp[u]!=ltp[v]){
if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
Modify(1,idx[ltp[u]],idx[u],add);
u=fa[ltp[u]];
}
if(dep[u]>dep[v])swap(u,v);
Modify(1,idx[u],idx[v],add);
}
int Opt2(int u,int v){
int res=0;
while(ltp[u]!=ltp[v]){
if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
res+=Query(1,idx[ltp[u]],idx[u]);
res%=p,u=fa[ltp[u]];
}
if(dep[u]>dep[v])swap(u,v);
return (res+Query(1,idx[u],idx[v]))%p;
}
void Opt3(int u,int add){
add%=p;
Modify(1,idx[u],idx[u]+siz[u]-1,add);
}
int Opt4(int u){
return Query(1,idx[u],idx[u]+siz[u]-1)%p;
}
signed main(){
Read(n),Read(m),Read(r),Read(p);
for(rg int i=1;i<=n;i++)Read(ipt[i]);
for(rg int i=1;i<n;i++){
int u,v;
Read(u),Read(v);
ade(u,v),ade(v,u);
}
DFS1(r,0);
DFS2(r,r);
Build(1,1,n);
for(rg int i=1;i<=m;i++){
int opt,x,y,z;
Read(opt),Read(x);
switch(opt){
case 1:{
Read(y),Read(z);
Opt1(x,y,z);
break;
}
case 2:{
Read(y);
cout<<Opt2(x,y)%p<<endl;
break;
}
case 3:{
Read(z);
Opt3(x,z);
break;
}
case 4:{
cout<<Opt4(x)%p<<endl;
break;
}
}
}
return 0;
}
有#define int LL
。
缺省源又臭又长放到其他blog里了。
3.总结
其实树剖听起来很毒瘤然而理解了就很简单了。
她可以解决许多想不出正解/十分毒瘤的树上问题。
树剖的代码看着长其实是数据结构长,树剖的核心代码并不长,而且很容易理解。