树连剖分

WPY神仙:什么时候能行云流水一次性过树链剖分啊!

转载于luogu日报

树链剖分就是将树分割成多条链,然后利用数据结构(线段树、树状数组等)来维护这些链。别说你不知道什么是树~~ ╮(─▽─)╭

前置知识

  • dfs序
  • LCA 线段树

先来回顾两个问题:

1.将树从(x)(y)结点最短路径上所有节点的值都加上z

这也是个模板题了吧

我们很容易想到,树上差分可以以(O(n+m))的优秀复杂度解决这个问题

2.求树从(x)(y)结点最短路径上所有节点的值之和

lca大水题,我们又很容易地想到,dfs(O(n))预处理每个节点的dis(即到根节点的最短路径长度)

然后对于每个询问,求出x,y两点的lca,利用lca的性质 distance $(x,y)=dis(x)+dis(y)-2*dis(lca) $求出结果

时间复杂度(O(mlogn+n))

现在来思考一个bug:

如果刚才的两个问题结合起来,成为一道题的两种操作呢?

刚才的方法显然就不够优秀了(每次询问之前要跑dfs更新dis)

树剖是通过轻重边剖分将树分割成多条链,然后利用数据结构来维护这些链(本质上是一种优化暴力)

首先明确概念:

  • 重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
  • 轻儿子:父亲节点中除了重儿子以外的儿子;
  • 重边:父亲结点和重儿子连成的边;
  • 轻边:父亲节点和轻儿子连成的边;
  • 重链:由多条重边连接而成的路径;
  • 轻链:由多条轻边连接而成的路径;

树连剖分

比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,

2-11就是重链,2-5就是轻链,用红点标记的就是该结点所在重链的起点,也就是下文提到的top结点,

还有每条边的值其实是进行dfs时的执行序号。

声明变量:

int cnt,lnk[maxn],nxt[maxn<<1],to[maxn<<1];//建边
int a[maxn<<2],lazy[maxn<<2];//线段树
int son[maxn],w[maxn],id[maxn],tot,deep[maxn],size[maxn],top[maxn],fa[maxn],rk[maxn];//son[maxn]:重儿子,w[maxn]:权值,id[maxn]:树上节点对应线段树上的标号,deep[maxn]:节点深度,size[maxn]节点子树大小,top[maxn]链最上端的编号,fa[maxn]父节点,rk[maxn]线段树上编号对应的树上节点

树链剖分的实现

1.对于一个点我们首先求出它所在的子树大小,找到它的重儿子(即处理出size,son数组)

解释:比如说点1,它有三个儿子2,3,4

2所在子树的大小是5

3所在子树的大小是2

4所在子树的大小是6

那么1的重儿子是4

ps:如果一个点的多个儿子所在子树大小相等且最大,那随便找一个当做它的重儿子就好了。

叶节点没有重儿子,非叶节点有且只有一个重儿子

2.在dfs过程中顺便记录其父亲以及深度(即处理出f,d数组),操作1,2可以通过一遍dfs完成

inline void DFS_1(int x,int f,int dep){
	deep[x]=dep;fa[x]=f;size[x]=1;
	int max_son=-1;
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==f)continue;
		DFS_1(to[j],x,dep+1);
		size[x]+=size[to[j]];
		if(size[to[j]]>max_son){max_son=size[to[j]],son[x]=to[j];}
	}
	return ;
}

树连剖分
dfs跑完大概是这样的,大家可以手动模拟一下

3.第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,并且为了用数据结构来维护重链,我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)

inline void DFS_2(int x,int tp){
	id[x]=++tot;
	rk[tot]=x;
	top[x]=tp;
	if(son[x])DFS_2(son[x],tp);
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==fa[x]||to[j]==son[x])continue;
		DFS_2(to[j],to[j]);
	}
	return ;
}

树连剖分

dfs跑完大概是这样的,大家可以手动模拟一下

4,两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息

回顾上文的那个题目,修改和查询操作原理是类似的,以查询操作为例,其实就是个LCA,不过这里使用了top来进行加速,因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,他们的top就是自己。需要注意的是,每次循环只能跳一次,并且让结点深的那个来跳到top的位置,避免两个一起跳从而插肩而过。

inline void build(int x,int L,int R){
	if(L==R){
		a[x]=w[rk[L]]%TT;
		return ;
	}
	int mid=(R-L>>1)+L;
	build(x<<1,L,mid);
	build(x<<1|1,mid+1,R);
	a[x]=(a[x<<1]+a[x<<1|1])%TT;
	return ;
}

inline void pushdown(int x,int len){
	if(lazy[x]==0)return ;
	lazy[x<<1]=(lazy[x<<1]+lazy[x])%TT;
	lazy[x<<1|1]=(lazy[x<<1|1]+lazy[x])%TT;
	a[x<<1]=(a[x<<1]+lazy[x]*(len-(len>>1)))%TT;
	a[x<<1|1]=(a[x<<1|1]+lazy[x]*(len>>1))%TT;
	lazy[x]=0;
	return ;
}

inline void query(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R){ret=(ret+a[x])%TT;return ;}
	else {
		pushdown(x,r-l+1);int mid=(r-l>>1)+l;
		if(L<=mid)query(x<<1,l,mid,L,R);
		if(R>mid)query(x<<1|1,mid+1,r,L,R);
	}
	return ;
}

inline void update(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		lazy[x]=(lazy[x]+k)%TT;
		a[x]=(a[x]+k*(r-l+1))%TT;
	}
	else{
		pushdown(x,r-l+1);
		int mid=(r-l>>1)+l;
		if(L<=mid)update(x<<1,l,mid,L,R,k);
		if(R>mid)update(x<<1|1,mid+1,r,L,R,k);
		a[x]=(a[x<<1]+a[x<<1|1])%TT;
	}
	return ;
}

inline int qRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		ret=0;
		query(1,1,N,id[top[x]],id[x]);
		ans=(ret+ans)%TT;
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	ret=0;query(1,1,N,id[x],id[y]);
	ans=(ans+ret)%TT;
	return ans;
}

inline void upRange(int x,int y,int k){
	k%=TT;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		update(1,1,N,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	update(1,1,N,id[x],id[y],k);
	return ;
}

inline int qson(int x){
	ret=0;
	query(1,1,N,id[x],id[x]+size[x]-1);
	return ret;
}
inline void upson(int x,int k){
	update(1,1,N,id[x],id[x]+size[x]-1,k);
}

大家如果明白了树链剖分,也应该有举一反三的能力(反正我没有),修改和LCA就留给大家自己完成了

5.树链剖分的时间复杂度

树链剖分的两个性质:

1,如果(u,v)是一条轻边,那么(size(v)<size(u)/2)

2,从根结点到任意结点的路所经过的轻重链的个数必定都小于(logn)

可以证明,树链剖分的时间复杂度为(O(nlogn))

例题

1.树链剖分模板

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
typedef long long LL;
int N,M,R,TT;
int  ret;
inline int read(){
	int ret=0,f=1;char ch=getchar();
	while(ch<'0'|ch>'9'){if(ch=='-')f=-f;ch=getchar();}
	while(ch<='9'&&ch>='0')ret=ret*10+ch-'0',ch=getchar();
	return ret*f;
}
int cnt,lnk[maxn],nxt[maxn<<1],to[maxn<<1],w[maxn];
int a[maxn<<2],lazy[maxn<<2];
int son[maxn],id[maxn],tot,deep[maxn],size[maxn],top[maxn],fa[maxn],rk[maxn];

inline void add_e(int x,int y){to[++cnt]=y;nxt[cnt]=lnk[x];lnk[x]=cnt;}

inline void DFS_1(int x,int f,int dep){
	deep[x]=dep;fa[x]=f;size[x]=1;
	int max_son=-1;
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==f)continue;
		DFS_1(to[j],x,dep+1);
		size[x]+=size[to[j]];
		if(size[to[j]]>max_son){max_son=size[to[j]],son[x]=to[j];}
	}
	return ;
}

inline void DFS_2(int x,int tp){
	id[x]=++tot;
	rk[tot]=x;
	top[x]=tp;
	if(son[x])DFS_2(son[x],tp);
	for(int j=lnk[x];j;j=nxt[j]){
		if(to[j]==fa[x]||to[j]==son[x])continue;
		DFS_2(to[j],to[j]);
	}
	return ;
}

inline void build(int x,int L,int R){
	if(L==R){
		a[x]=w[rk[L]]%TT;
		return ;
	}
	int mid=(R-L>>1)+L;
	build(x<<1,L,mid);
	build(x<<1|1,mid+1,R);
	a[x]=(a[x<<1]+a[x<<1|1])%TT;
	return ;
}

inline void pushdown(int x,int len){
	if(lazy[x]==0)return ;
	lazy[x<<1]=(lazy[x<<1]+lazy[x])%TT;
	lazy[x<<1|1]=(lazy[x<<1|1]+lazy[x])%TT;
	a[x<<1]=(a[x<<1]+lazy[x]*(len-(len>>1)))%TT;
	a[x<<1|1]=(a[x<<1|1]+lazy[x]*(len>>1))%TT;
	lazy[x]=0;
	return ;
}

inline void query(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R){ret=(ret+a[x])%TT;return ;}
	else {
		pushdown(x,r-l+1);int mid=(r-l>>1)+l;
		if(L<=mid)query(x<<1,l,mid,L,R);
		if(R>mid)query(x<<1|1,mid+1,r,L,R);
	}
	return ;
}

inline void update(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		lazy[x]=(lazy[x]+k)%TT;
		a[x]=(a[x]+k*(r-l+1))%TT;
	}
	else{
		pushdown(x,r-l+1);
		int mid=(r-l>>1)+l;
		if(L<=mid)update(x<<1,l,mid,L,R,k);
		if(R>mid)update(x<<1|1,mid+1,r,L,R,k);
		a[x]=(a[x<<1]+a[x<<1|1])%TT;
	}
	return ;
}

inline int qRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		ret=0;
		query(1,1,N,id[top[x]],id[x]);
		ans=(ret+ans)%TT;
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	ret=0;query(1,1,N,id[x],id[y]);
	ans=(ans+ret)%TT;
	return ans;
}

inline void upRange(int x,int y,int k){
	k%=TT;
	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		update(1,1,N,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	update(1,1,N,id[x],id[y],k);
	return ;
}

inline int qson(int x){
	ret=0;
	query(1,1,N,id[x],id[x]+size[x]-1);
	return ret;
}
inline void upson(int x,int k){
	update(1,1,N,id[x],id[x]+size[x]-1,k);
}
int main(){
	N=read();M=read();R=read();TT=read();
	for(int i=1;i<=N;i++)w[i]=read();
	for(int i=1;i<N;i++){
		int x=read(),y=read();
		add_e(x,y);add_e(y,x);
	}
	DFS_1(R,0,1);
	DFS_2(R,R);
	build(1,1,N);
	for(int i=1;i<=M;i++){
		int p,x,y,k;
		p=read();
		if(p==1){x=read(),y=read(),k=read();upRange(x,y,k);}
		if(p==2){x=read();y=read();printf("%d
",qRange(x,y));}
		if(p==3){x=read();k=read();upson(x,k);}
		if(p==4){x=read();printf("%d
",qson(x));}
	}
	return 0;
}

WPY神仙:什么时候能行云流水一次性过树链剖分啊!

前后呼应