dfs序+树链剖分,超详细讲解+原理分析+模板(看不懂来打我)

dfs序+树链剖分

一. 树链剖分能解决什么问题?

在这里插入图片描述

二.树链剖分前置知识

1.dfs序

在这里插入图片描述

2. 时间戳

按照dfs第一次访问的顺序,给每一个节点标记上时间戳
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210707164442392.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RoZVdheUZvckRyZWFt,size_16,color_FFFFFF,t_70

3.dfs序和时间戳有什么用处

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
开始剖分,跑第二次dfs的作用,也即dfs1和dfs2
在这里插入图片描述
于是你需要这些数组来记录

struct Edge{
    
    
	int  v;//节点编号 
	int next;
}edge[Maxn*2];//注意开二倍,因为是无相边 
int head[Maxn];

int fa[Maxn];//记录父节点 
int dep[Maxn];//记录节点深度 
int son[Maxn];//记录重儿子
int siz[Maxn];//记录以该节点为根节点的树的大小(节点个数包括根节点) 
int top[Maxn];//每一个节点所属重链的根节点 
int dfn[Maxn];//每一个节点的时间戳
int w[Maxn];//dfs序后节点的权值,用线段树维护
int tim = 0;//时间戳计数器
int cnt = 0;
int v[Maxn];//存放所有节点的权值 


int sum[Maxn*4];//线段树区间数组维护w[]区间和 
int lazy[Maxn*4];//维护区间加的延迟数据,以便于延迟下方 
int lpos[Maxn*4],rpos[Maxn*4];//线段树区间的左右端点

链式前向星存图:

/*链式前向星建图,无向边*/
void build(int u,int v){
    
    
	edge[++cnt].v = v;
    edge[cnt].next = head[u];
    head[u] = cnt;
    edge[++cnt].v = u;
    edge[cnt].next = head[v];
    head[v] = cnt;
}

dfs1

/*报一遍dfs记录重儿子,节点深度,以及树的大小*/
void dfs1(int u,int f){
    
    
	fa[u] = f;
	dep[u] = dep[f]+1;
	siz[u] = 1;
	int maxsonsize = -1;//记录重儿子的大小
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		int v=edge[i].v;//与u有边的节点 
		if(v==f) continue;//如果v时u的父亲,直接跳过
		/*否则就是u的儿子,dfs下去*/
		dfs1(v,u);
		siz[u]+=siz[v];
		/*更新u的重儿子*/
		if(siz[v]>maxsonsize){
    
    
			maxsonsize = siz[v];
			son[u] = v;
		}
	} 
}

dfs2

/*再跑一边dfs,完成树链剖分*/
void dfs2(int u,int t){
    
    
	dfn[u] = ++tim;//dfs序 
	top[u] = t;//u所属重链的祖先节点
	w[tim] = v[u];//dfs序后的节点权值
	/*没有重儿子,代表时根节点,直接return*/
	if(!son[u]) return ;
	dfs2(son[u],t);
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		int v = edge[i].v;
		//如果v时u的父节点,或者v时u的重儿子(已经遍历过了) 
		if(v==fa[u]||v==son[u]) continue;
		/*否则以该节点为新的重链的祖先,继续dfs序*/
		else dfs2(v,v); 
	}
	return ;
}

线段树的区间修改和查询,对链进行操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
操作1
在这里插入图片描述
操作2
和操作1有一点不同,把modif()改为query()即可
在这里插入图片描述

三.树链剖分练习题

1.模板题

AC代码:

/*轻重链剖分/树链剖分*/
#include<bits/stdc++.h>
#define ls dep<<1
#define rs dep<<1|1
using namespace std;
const int Maxn = 1e5+10;
int N,M,R,P;
struct Edge{
    
    
	int  v;//节点编号 
	int next;
}edge[Maxn*2];
int head[Maxn];
int fa[Maxn];//记录父节点 
int dep[Maxn];//记录节点深度 
int son[Maxn];//记录重儿子
int siz[Maxn];//记录以该节点为根节点的树的大小(节点个数包括根节点) 
int top[Maxn];//每一个节点所属重链的根节点 
int dfn[Maxn];//每一个节点的时间戳
int w[Maxn];//dfs序后节点的权值,用线段树维护

int sum[Maxn*4];//线段树区间数组维护w[]区间和 
int lazy[Maxn*4];//维护区间加的延迟数据,以便于延迟下方 
int lpos[Maxn*4],rpos[Maxn*4];//线段树区间的左右端点 
int tim = 0;//时间戳计数器
int cnt = 0;
int v[Maxn];//存放所有节点的权值 
/*链式前向星建图,双向边*/
void build(int u,int v){
    
    
	edge[++cnt].v = v;
    edge[cnt].next = head[u];
    head[u] = cnt;
    edge[++cnt].v = u;
    edge[cnt].next = head[v];
    head[v] = cnt;
}
/*报一遍dfs记录重儿子,深度,以及子树大小*/
void dfs1(int u,int f){
    
    
	fa[u] = f;
	dep[u] = dep[f]+1;
	siz[u] = 1;
	int maxsonsize = -1;//记录重儿子的大小
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		int v=edge[i].v;//与u有边的节点 
		if(v==f) continue;//如果v时u的父亲,直接跳过
		/*否则就是u的儿子,dfs下去*/
		dfs1(v,u);
		siz[u]+=siz[v];
		/*更新u的重儿子*/
		if(siz[v]>maxsonsize){
    
    
			maxsonsize = siz[v];
			son[u] = v;
		}
	} 
}

/*再跑一边dfs,完成树链剖分*/
void dfs2(int u,int t){
    
    
	dfn[u] = ++tim;//dfs序 
	top[u] = t;//u所属重链的祖先节点
	w[tim] = v[u];//dfs序后的节点权值
	/*没有重儿子,代表时根节点,直接return*/
	if(!son[u]) return ;
	dfs2(son[u],t);
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		int v = edge[i].v;
		//如果v时u的父节点,或者v时u的重儿子(已经遍历过了) 
		if(v==fa[u]||v==son[u]) continue;
		/*否则以该节点为新的重链的祖先,继续dfs序*/
		else dfs2(v,v); 
	}
	return ;
}

/*更新区间和*/
void pushup(int dep){
    
    
	sum[dep] = sum[ls]%P+sum[rs]%P;
	sum[dep]%=P;
	return ;
}
/*延迟更新,标记下放操作*/
void pushdown(int dep){
    
    
	if(lazy[dep]){
    
    
		lazy[rs]+=lazy[dep];
		lazy[ls]+=lazy[dep];
		lazy[rs]%=P;
		lazy[ls]%=P;
		sum[ls]+=lazy[dep]%P*(rpos[ls]-lpos[ls]+1)%P;
		sum[rs]+=lazy[dep]%P*(rpos[rs]-lpos[rs]+1)%P;
		sum[ls]%=P;
		sum[rs]%=P;
		lazy[dep] = 0;
	}
	return ;
}
/*建立线段树*/
void build_tree(int l,int r,int dep){
    
    
	if(l==r){
    
    
		sum[dep] = w[l]%P;
		sum[dep]%=P;
		lpos[dep]=rpos[dep]=l;
		return ;
	}
	int mid = l+r>>1;
	build_tree(l,mid,ls);
	build_tree(mid+1,r,rs);
	lpos[dep] = l;
	rpos[dep] = r;
	pushup(dep);//更新区间和 
}
void modify(int l,int r,int ql,int qr,int dep,int val){
    
    
	if(ql<=l&&r<=qr){
    
    
		sum[dep]+=(r-l+1)%P*val%P;
		sum[dep]%=P;
		lazy[dep]+=val%P;
		lazy[dep]%=P;
		return ;
	}
	pushdown(dep);
	int mid = l+r>>1;
	if(ql<=mid) modify(l,mid,ql,qr,ls,val);
	if(qr>mid) modify(mid+1,r,ql,qr,rs,val);
	pushup(dep);
	return ;
}
/*区间查询*/
int query(int l,int r,int ql,int qr,int dep){
    
    
	if(ql<=l&&r<=qr){
    
    
		return sum[dep]%P;
	}
	pushdown(dep);
	int mid = l+r>>1;
	int ans = 0;
	if(ql<=mid) ans+=query(l,mid,ql,qr,ls);
	if(qr>mid) ans+=query(mid+1,r,ql,qr,rs);
	ans%=P;
	return ans;
}
/*初始化函数*/
void init(){
    
    
	memset(head,-1,sizeof(head));
	return ;
}
/*操作1:x-y链上的所有节点加上z*/
void modifychain(int x,int y,int z){
    
    
	z%=P;//因为答案要模P,所以先对z取模 
	
	/*节点x和y不在同一条重链上*/
	while(top[x]!=top[y]){
    
    
		/*先修改重链祖先更深的点,这样才能不断向上走合并为,
		转移到一条重链上最后(有点抽象,画个图)*/
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		modify(1,N,dfn[top[x]],dfn[x],1,z);//区间修改
		x = fa[top[x]];	 
	}
	if(dep[x]>dep[y]) swap(x,y);
	modify(1,N,dfn[x],dfn[y],1,z);
	return ;
}
/*操作2:树从 x 到 y 结点最短路径上所有节点的值之和*/
int querychain(int x,int y){
    
    
	int ans = 0;
	/*节点x和y不在同一条重链上*/
	while(top[x]!=top[y]){
    
    
		/*先修改重链祖先更深的点,这样才能不断向上走合并为,
		转移到一条重链上最后(有点抽象,画个图)*/
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		ans+=query(1,N,dfn[top[x]],dfn[x],1);//区间修改
		ans%=P;
		x = fa[top[x]];	 
	}
	if(dep[x]>dep[y]) swap(x,y);
	ans+=query(1,N,dfn[x],dfn[y],1);
	ans%=P;
	return ans;
}
/*操作3:以x为根节点的子数内所有节点值都加上z*/
void modifyson(int x,int z){
    
    
	/*因为一个节点的子树上的时间戳一定大于该节点,并且连续*/
	modify(1,N,dfn[x],dfn[x]+siz[x]-1,1,z);//线段树维护的w[]数组的区间修改 
} 
/*操作4:求以x为根节点的子数内所有节点值之和*/
int queryson(int x){
    
    
	return query(1,N,dfn[x],dfn[x]+siz[x]-1,1)%P;
}
/*
void t_sum(int l,int r,int dep){
	if(l==r){
		cout<<sum[dep]<<' '<<dep<<'\n';
		return ;
	}
	int mid = l+r>>1;
	t_sum(l,mid,ls);
	t_sum(mid+1,r,rs);
	return ;
}*/
int main()
{
    
    
	init();
	cin>>N>>M>>R>>P;//表示树的结点个数、操作个数、根节点序号和取模数
	for(int i=1;i<=N;i++) cin>>v[i];
    for(int i=1;i<N;i++){
    
    
    	int u,v;
    	cin>>u>>v;
    	build(u,v);
	}	
	dfs1(R,R);
	dfs2(R,R);
	build_tree(1,N,1);
	for(int i=1;i<=M;i++){
    
    
		int op;
		cin>>op;
		if(op==1){
    
    
			int x,y,z;
			cin>>x>>y>>z;
			modifychain(x,y,z);
		}
		else if(op==2){
    
    
			int x,y;
			cin>>x>>y;
			cout<<querychain(x,y)%P<<'\n';
		}
		else if(op==3){
    
    
			int x,z;
			cin>>x>>z;
			modifyson(x,z);
		}
		else if(op==4){
    
    
			int x;
			cin>>x;
			cout<<queryson(x)%P<<'\n';	
		}
	}
	return 0;
}

2.树链剖分求最近公共祖先LCA

AC代码:

#include<bits/stdc++.h>
using namespace std;
const int Maxn = 5e5+10;
struct Edge{
    
    
	int v;
	int next;	
}edge[Maxn*2];
int head[Maxn];
int cnt = 0;
/*链式前向星建图*/
void build(int u,int v){
    
    
   edge[++cnt].v=v;
   edge[cnt].next=head[u];
   head[u] = cnt;
   edge[++cnt].v = u;
   edge[cnt].next = head[v];
   head[v] = cnt;
   return ;
}
int fa[Maxn];//父节点 
int son[Maxn];//重儿子 
int siz[Maxn];//树的大小
int dep[Maxn];//节点深度
int dfn[Maxn];//节点的时间戳
int top[Maxn];//每一条重链的根节点
int tim;//时间戳计数器
int N,M,S;
//第一次dfs找出重儿子,节点深度,树的大小 
void dfs1(int u,int f){
    
    
	fa[u] = f;
	dep[u] = dep[f]+1;
	siz[u] = 1;
	int maxsonsize = -1;
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		int v = edge[i].v;
		/*如果v是u的父节点,continue*/
		if(v==f) continue;
		/*否则就是u的儿子*/
		dfs1(v,u);
		siz[u]+=siz[v];//更新u节点的大小
		/*更新u节点的重儿子*/
		if(siz[v]>maxsonsize){
    
    
		  maxsonsize = siz[v];
		  son[u] = v;	
		}  
	}
	return ; 
} 
/*跑第二遍dfs,完成树链剖分,轻重链剖分
找出节点时间戳,重链的祖先节点*/
void dfs2(int u,int t){
    
    
	dfn[u] = ++tim;
	top[u] = t;
	//cout<<u<<' '<<t<<'\n';
	/*u没有重儿子*/
	if(!son[u]) return ;
	
	dfs2(son[u],t);
	for(int i=head[u];i!=-1;i=edge[i].next){
    
    
		
		int v = edge[i].v;
		//cout<<u<<' '<<v<<'\n';
		/*如果v是u的父亲continue,如果v是u的重儿子(已经dfs过continue)*/
		if(v==fa[u]||v==son[u]) continue;
		/*否则v是u的轻儿子之一,以这个节点作为新重链的祖先节点*/
		dfs2(v,v);
	}
	return ;	
}
/*找x和y的最近公共祖先*/
void solve(int x,int y){
    
    
	/*不在一条重链上*/
	while(top[x]!=top[y]){
    
    
		/*找到深的一条链的祖先*/
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=fa[top[x]]; 
	}
	/*在一条重链上之后,谁知在上面谁是最近公共祖先*/
	if(dep[x]<dep[y]) cout<<x;
	else cout<<y;
	cout<<'\n';
	return ; 
}
void init(){
    
    
	memset(head,-1,sizeof(head));
	return ;
}

int main(){
    
    
	init();
	cin>>N>>M>>S;//分别表示树的结点个数、询问的个数和树根结点的序号
	for(int i=1;i<N;i++){
    
    
		int u,v;
		cin>>u>>v;
		build(u,v);
	}
	//cout<<1<<'\n';
	dfs1(S,S);
	dfs2(S,S);
	//cout<<top[2];
	//cout<<1<<'\n';
	for(int i=1;i<=M;i++){
    
    
		int x,y;
		cin>>x>>y;
		solve(x,y);
	}
	return 0;
} 

四.视频资源讲解

树链剖分视频讲解

本博客采用了某站博主的视频页面,如有冒犯请联系本人,谢谢!

猜你喜欢

转载自blog.csdn.net/TheWayForDream/article/details/118551255