写在前面
首先,在学树链剖分之前最好先把 LCA、树形DP、DFS序 这三个知识点学了
emm还有必备的 链式前向星、线段树 也要先学了。
如果这三个知识点没掌握好的话,树链剖分难以理解也是当然的。
树链剖分
树链剖分 就是对一棵树分成几条链,把树形变为线性,减少处理难度
需要处理的问题:
- 将树从x到y结点最短路径上所有节点的值都加上z
- 求树从x到y结点最短路径上所有节点的值之和
- 将以x为根节点的子树内所有节点值都加上z
- 求以x为根节点的子树内所有节点值之和
目录:
概念
- 重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子
- 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
- 叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
- 重边:连接任意两个重儿子的边叫做重边
- 轻边:剩下的即为轻边
- 重链:相邻重边连起来的 连接一条重儿子 的链叫重链
- 对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
- 每一条重链以轻儿子为起点
dfs1()
这个dfs要处理几件事情:
- 标记每个点的深度dep[]
- 标记每个点的父亲fa[]
- 标记每个非叶子节点的子树大小(含它自己)
标记每个非叶子节点的重儿子编号son[]
建议可以先做一下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(Rint i=beg[x];i;i=nex[i]){ int y=to[i]; if(y==f)continue;//若为父亲则continue dfs1(y,x,deep+1);//dfs其儿子 siz[x]+=siz[y];//把它的儿子数加到它身上 if(siz[y]>maxson)son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号 } }//变量解释见最下面
【Luogu3478】【POI2008】STA-Station(动态规划)(原理相同)
dfs2()
这个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(Rint i=beg[x];i;i=nex[i]){
int y=to[i];
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
}//变量解释见最下面
处理问题
Attention 重要的来了!!!
前面说到dfs2的顺序是先处理重儿子再处理轻儿子
我们来模拟一下:
- 因为顺序是先重再轻,所以每一条重链的新编号是连续的
- 因为是dfs,所以每一个子树的新编号也是连续的
现在回顾一下我们要处理的问题
- 处理任意两点间路径上的点权和
- 处理一点及其子树的点权和
- 修改任意两点间路径上的点权
- 修改一点及其子树的点权
1、当我们要处理任意两点间路径时:
设所在链顶端的深度更深的那个点为x点
- ans加上x点到x所在链顶端 这一段区间的点权和
- 把x跳到x所在链顶端的那个点的上面一个点
不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可
这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和
每次查询时间复杂度为O(log2n)O(log2n)
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);//把x点改为所在链顶端的深度更深的那个点
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=mod;//按题意取模
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%mod;
}//变量解释见最下面
2、处理一点及其子树的点权和:
想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的
于是直接线段树区间查询即可
时间复杂度为O(logn)O(logn)
inline int qSon(int x){
res=0;
query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1
return res;
}
当然,区间修改就和区间查询一样的啦~~
inline void updRange(int x,int y,int k){
k%=mod;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],k);
}
inline void updSon(int x,int k){
update(1,1,n,id[x],id[x]+siz[x]-1,k);
}//变量解释见最下面
建树
既然前面说到要用线段树,那么按题意建树就可以啦!
不过,建树这一步当然是在处理问题之前哦~
AC代码:
inline void updRange(int x,int y,int k){
k%=mod;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],k);
}
inline void updSon(int x,int k){
update(1,1,n,id[x],id[x]+siz[x]-1,k);
}//变量解释见最下面
然后换一种码风:
#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int read(){
char c;int x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
int n,m,r,q,cnt,rec;
int son[MAXN],rank[MAXN],tid[MAXN],nxt[MAXN<<1],head[MAXN],top[MAXN],fa[MAXN];
int to[MAXN<<1],val[MAXN],sum[MAXN<<2],add[MAXN<<2],dep[MAXN],siz[MAXN];
void addedge(int x,int y){
to[rec]=y;nxt[rec]=head[x];head[x]=rec;rec++;
to[rec]=x;nxt[rec]=head[y];head[y]=rec;rec++;
}
void up(int node){sum[node]=(sum[node<<1]+sum[node<<1|1])%q;}
void down(int node,int ls,int rs){ //下推标记
if(add[node]){
add[node<<1]+=add[node];
add[node<<1|1]+=add[node];
sum[node<<1]+=add[node]*ls;
sum[node<<1|1]+=add[node]*rs;
sum[node<<1]%=q;sum[node<<1|1]%=q;
add[node]=0;
}
}
void dfsI(int x,int father){ //第一个DFS
fa[x]=father;son[x]=-1;
dep[x]=dep[fa[x]]+1;siz[x]=1;
for(int i=head[x];i!=-1;i=nxt[i]){
int go=to[i];
if(go==fa[x]) continue;
dfsI(go,x);siz[x]+=siz[go];
if(son[x]==-1||siz[go]>siz[son[x]]) son[x]=go;
}
}
void dfsII(int x,int t){ //第二个DFS
tid[x]=++cnt;
rank[cnt]=val[x]%q;
top[x]=t;
if(son[x]==-1) return;
dfsII(son[x],t);
for(int i=head[x];i!=-1;i=nxt[i]){
int go=to[i];
if(go==fa[x]||go==son[x]) continue;
dfsII(go,go);
}
}
void build(int node,int l,int r){ //建树
if(l==r){
sum[node]=rank[l];
return;
}
int mid=(l+r)>>1;
build(node<<1,l,mid);
build(node<<1|1,mid+1,r);
up(node);
}
void update(int node,int l,int r,int L,int R,int ad){ //线段树区间修改
if(L<=l&&r<=R){
sum[node]+=ad*(r-l+1);sum[node]%=q;
add[node]+=ad;return;
}
int mid=(l+r)>>1;
down(node,mid-l+1,r-mid);
if(L<=mid) update(node<<1,l,mid,L,R,ad);
if(R>mid) update(node<<1|1,mid+1,r,L,R,ad);
up(node);
}
int query(int node,int l,int r,int L,int R){ //区间求和
if(L<=l&&r<=R){
return sum[node];
}
int mid=(l+r)>>1,ans=0;
down(node,mid-l+1,r-mid);
if(L<=mid) ans+=query(node<<1,l,mid,L,R);
if(R>mid) ans+=query(node<<1|1,mid+1,r,L,R);
ans%=q;
return ans;
}
int qTree(int x){ //这是操作4
return query(1,1,n,tid[x],tid[x]+siz[x]-1);
}
void upTree(int x,int ad){ //操作3
update(1,1,n,tid[x],tid[x]+siz[x]-1,ad);
}
int queryLink(int x,int y){ //DFS求链上权值和
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]) swap(x,y);
ans+=query(1,1,n,tid[top[y]],tid[y]);
ans%=q;
y=fa[top[y]];
}
if(dep[x]>dep[y]) swap(x,y);
ans+=query(1,1,n,tid[x],tid[y]);
ans%=q;
return ans;
}
void updateLink(int x,int y,int ad){ //链上修改
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]) swap(x,y);
update(1,1,n,tid[top[y]],tid[y],ad);
y=fa[top[y]];
}
if(dep[x]>dep[y]) swap(x,y);
update(1,1,n,tid[x],tid[y],ad);
}
int main()
{
memset(head,-1,sizeof(head));
n=read();m=read();r=read();q=read();
for(int i=1;i<=n;i++) val[i]=read();
for(int i=1;i<=n-1;i++){
int x=read(),y=read();
addedge(x,y);
}
dfsI(r,0);
dfsII(r,r);
build(1,1,n);
for(int i=1;i<=m;i++){
int x=read();
if(x==1){
int u=read(),v=read(),ad=read();
updateLink(u,v,ad);
}
if(x==2){
int u=read(),v=read();
printf("%d\n",queryLink(u,v));
}
if(x==3){
int u=read(),ad=read();
upTree(u,ad);
}
if(x==4){
int u=read();
printf("%d\n",qTree(u));
}
}
return 0;
}