树链剖分/重链剖分学习笔记

定义/解释

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

树链剖分(树剖/链剖)有多种形式,如 重链剖分长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。

重链剖分可以将树上的任意一条路径划分成不超过log(n)条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。

重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

(内容来自OI Wiki)

一些关于树链剖分的基础概念

重儿子:一个节点的所有儿子中子树最大的儿子

轻儿子:一个节点的所有儿子中除去重儿子之外的儿子

重边:一个节点与它重儿子的边

重链:由若干条重边组成的链

轻链:重链以外的链

总而言之,树链剖分的主要思想就是将树分割成若干条链,将其转换为线性结构,再使用各种不同的数据结构来维护树上路径的各种信息。

第一部分:如何分割

那既然叫重链剖分,肯定是通过重链来分割的。我们先通过定义来画一下重链:

(图中我们将重链标成了红色)

我们保留所有的重链,将剩下的边删去。

 

现在我们发现,这棵树被分割成了4条单独的链。那么这也就是树链剖分的分割过程。

也就是说,我们求出一棵树所有的重链,就能把这棵树分割开。 但是考虑到后面的维护操作,我们在分割的过程中不是仅仅求出重链即可,其实还要求很多其他的东西。

那么,让我们想想我们需要什么。

对于一条重链,我们需要知道它的链顶的节点,方便我们维护树上路径;我们还需要一个在重链上能够连续的新编号,因为我们肯定没法通过原来的编号对重链上的点进行操作;根据重链的定义,我们还必须求出每个节点的子树大小,不然我们就没办法找出节点的重儿子;另外,对于树上问题,求出一个节点的深度父亲是必要的。

那么,总结一下我们需要的东西:

1.每个节点的深度,父亲

2.每个节点的子树大小

3.每个节点的重儿子

4.每条重链的链顶节点

5.一个新的连续的dfs序

然后我们就可以通过dfs来求这些内容了,在代码实现上,我们选择跑两遍dfs,第一遍求每个节点的深度、父亲、子树大小、重儿子;第二遍求重链的链顶节点一个新的dfs序,同时依据题目维护必要的树上信息。

我们先来看第一个dfs

​
void dfs1(int x,int fa,int dep) //很经典的dfs 
{
	f[x]=fa;       //父亲
	depth[x]=dep;  //深度
	size[x]=1;     //子树大小(初始为1)
	for(int i=head[x];i!=-1;i=Next[i])
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs1(y,x,dep+1);
		size[x]+=sizes[y];  //回溯记录子树大小
		if(size[y]>size[hson[x]]) //hson初始化为0 sizes[0]=0 第一次比较时不会出问题 
		{
			hson[x]=y; //取子树大小最大的儿子为重儿子 
		}
	}
}

dfs1(root,-1,1);  //调用

​

其实除了求重儿子的部分,其他都是很简单的树上dfs

还是解释一下

f数组记录父亲 depth数组记录深度 sizes数组记录子树大小

hson数组记录每个节点的重儿子,在回溯时我们求出每个节点的子树大小,同时将节点的儿子中子树最大的儿子作为该节点的重儿子。

然后我们再看第二个dfs

void dfs2(int x,int t) //t表示 x所在重链的链顶 
{
	tops[x]=t;       //链顶 
	dfn[x]=++idc;    //dfs序
	
	if(hson[x]==0) return ; 
	dfs2(hson[x],t);  
	//优先遍历重儿子 保证重链编号连续 方便维护 
	for(int i=head[x];i!=-1;i=Next[i])
	{
		int y=ver[i];
		if(y==f[x]||y==hson[x]) continue; //再往后我们只记录轻链
		dfs2(y,y); //轻链的链顶就是它自己 
	}
}

dfs2(root,root) //调用 最开始链顶就是root

注释还是比较详细的。

注意我们这里优先遍历重链的目的是使得每条重链上的dfs序是连续的,这样就方便我们后续用数据结构来维护重链上信息。

那么现在这棵树我们就分割完啦(*^▽^*)

第二部分:如何维护

嗯...等等,我们要维护什么?

当然是具体题目具体分析啦

举个例子,我们维护树上两点之间最短路径的权值和(带修改的)

我们考虑线段树维护

那么代码就长这样:

void Tree_path_update(int x,int y,int z)  //修改路径上的点权 
{
	//最短路径的本质就是求LCA 我们在让x和y向LCA上跳的同时维护区间修改 
	while(tops[x]!=tops[y])    //如果说x和y不在同一条重链上 我们就上跳       
	{
		if(depth[tops[x]]<depth[tops[y]]) swap(x,y); //我们先跳 重链链顶深度较大的点 (这里我们令x为深度较大的点) 
		update(1,dfn[tops[x]],dfn[x],z);  //在上跳前维护这个区间修改 注意区间左端点小于右端点  
		x=f[tops[x]];	//我们从x跳到它重链链顶的父亲  
	} //重复操作直到两者在同一条重链上
	//其实此时两者不一定在同一个点上 所以再维护一次 (即使在一个点上 这个点也还没有修改)
	if(depth[x]>depth[y]) swap(x,y); //保证维护区间左端点小于右端点 
	update(1,dfn[x],dfn[y],z);
}

我感觉代码注释挺详细的,我就不用再解释了

这里代码的本质还是维护重链,再用类似LCA的方法把路径连起来,此处注意一些细节:

上跳的时候是让重链链顶深度较大的点先跳(因为每次会跳到链顶的父亲),这里一定是比较链顶的深度。

if(depth[tops[x]]<depth[tops[y]]) swap(x,y);

然后就是注意跳到同一条重链后两点不一定是同一点,还要再算一次。(线段树维护始终记得操作区间左端点小于等于右端点)

然后就完成啦

第三部分:如何查询

我是来凑数的awa

这其实是线段树的部分,操作和区间修改是一样的,这里仅给出代码

int Tree_path_query(int x,int y) //查询路径点权 操作同修改 不做解释 
{
	int ans=0;
	while(tops[x]!=tops[y])
	{
		if(depth[tops[x]]<depth[tops[y]]) swap(x,y);
		ans+=query(1,dfn[tops[x]],dfn[x])%p;
		x=f[tops[x]];
	}
	if(depth[x]>depth[y]) swap(x,y);
	ans+=query(1,dfn[x],dfn[y])%p;
	return ans%p;
}

第四部分:模板题

P3384-链接在这里

模板题的大部分内容前面已经讲完了,这里强调几点细节

1.这个题一定要取模

2.这里我们线段树维护的时候一定要注意,我们维护的是dfs之后重新编号的节点权值和,这一点在我们跑dfs时要重新建一个数组记录重新编号之后的点权,然后再建树

3.维护最短路径下权值和的操作前面已经给出,对于维护节点子树权值和的操作,我们直接进行线段树维护,因为一个点子树的编号是连续的。具体内容看代码

最后,给出模板题的详细注解AC代码

//树链剖分-重链剖分 
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=2e5+10;
int n,m,p,root;
int head[N],ver[M],Next[M],tot=-1;
int a[N],w[N];  //每个点的点权

int f[N],depth[N],sizes[N],hson[N]; 
//记录每个点的父亲 深度 子树大小 重儿子 dfs1
int tops[N],ranks[N],dfn[N],idc=0;
//记录每个点  所在重链的链顶  dfs序与节点编号的对应关系  dfs序 
 
struct node
{
	int left,right;
	int dist,lazy;
}tree[N*4];
void add(int x,int y)
{
	++tot;
	ver[tot]=y;
	Next[tot]=head[x];
	head[x]=tot;
}
void build(int x,int l,int r)
{
	tree[x].left=l;
	tree[x].right=r;
	if(l==r)
	{
		tree[x].dist=w[l]%p; //建树要按照dfs序建树 
		tree[x].lazy=0;
		return ;
	}
	int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	tree[x].dist=(tree[x*2].dist+tree[x*2+1].dist)%p;
}
void pushlazy(int x)
{
	if(tree[x].lazy==0) return ;
	int laz=tree[x].lazy;
	tree[x*2].dist+=((tree[x*2].right-tree[x*2].left+1)*laz)%p;
	tree[x*2].lazy+=laz;
	tree[x*2+1].dist+=((tree[x*2+1].right-tree[x*2+1].left+1)*laz)%p;
	tree[x*2+1].lazy+=laz;
	tree[x].lazy=0;
	return ;
}
void update(int x,int l,int r,int k)
{
	if(tree[x].left>=l&&tree[x].right<=r)
	{
		tree[x].dist+=((tree[x].right-tree[x].left+1)*k)%p;
		tree[x].lazy+=k;
		return ;
	}
	pushlazy(x);
	int mid=(tree[x].left+tree[x].right)/2;
	if(l<=mid)
	{
		update(x*2,l,r,k);
	}
	if(mid+1<=r)
	{
		update(x*2+1,l,r,k);
	}
	tree[x].dist=(tree[x*2].dist+tree[x*2+1].dist)%p;
}
int query(int x,int l,int r)
{
	if(tree[x].left>=l&&tree[x].right<=r)
	{
		return tree[x].dist%p;
	}
	pushlazy(x);
	int mid=(tree[x].left+tree[x].right)/2;
	int res=0;
	if(l<=mid)
	{
		res+=query(x*2,l,r)%p;
	}
	if(mid+1<=r)
	{
		res+=query(x*2+1,l,r)%p;
	}
	return res%p;
}
void dfs1(int x,int fa,int dep) //很经典的dfs 
{
	f[x]=fa;
	depth[x]=dep;
	sizes[x]=1;
	for(int i=head[x];i!=-1;i=Next[i])
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs1(y,x,dep+1);
		sizes[x]+=sizes[y];
		if(sizes[y]>sizes[hson[x]]) //sizes[0]=0 比较时不会出问题 
		{
			hson[x]=y; //取子树大小最大的儿子为重儿子 
		}
	}
}
void dfs2(int x,int t) //t表示 x所在重链的链顶 
{
	tops[x]=t;       //链顶 
	dfn[x]=++idc;    //dfs序
	w[idc]=a[x];     //将原数组按dfs序映射到新数组上 方便线段树维护 
	
	if(hson[x]==0) return ; 
	dfs2(hson[x],t);  
	//优先遍历重儿子 保证重链编号连续 方便维护 
	for(int i=head[x];i!=-1;i=Next[i])
	{
		int y=ver[i];
		if(y==f[x]||y==hson[x]) continue; //再往后我们只记录轻链
		dfs2(y,y); //轻链的链顶就是它自己 
	}
}
void Tree_path_update(int x,int y,int z)  //修改路径上的点权 
{
	//最短路径的本质就是求LCA 我们在让x和y向LCA上跳的同时维护区间修改 
	while(tops[x]!=tops[y])    //如果说x和y不在同一条重链上 我们就上跳       
	{
		if(depth[tops[x]]<depth[tops[y]]) swap(x,y); //我们先跳 重链链顶深度较大的点 (这里我们令x为深度较大的点) 
		update(1,dfn[tops[x]],dfn[x],z);  //在上跳前维护这个区间修改 注意区间左端点小于右端点  
		x=f[tops[x]];	//我们从x跳到它重链链顶的父亲  
	} //重复操作直到两者在同一条重链上
	//其实此时两者不一定在同一个点上 所以再维护一次 (即使在一个点上 这个点也还没有修改)
	if(depth[x]>depth[y]) swap(x,y); //保证维护区间左端点小于右端点 
	update(1,dfn[x],dfn[y],z);
}
int Tree_path_query(int x,int y) //查询路径点权 操作同修改 不做解释 
{
	int ans=0;
	while(tops[x]!=tops[y])
	{
		if(depth[tops[x]]<depth[tops[y]]) swap(x,y);
		ans+=query(1,dfn[tops[x]],dfn[x])%p;
		x=f[tops[x]];
	}
	if(depth[x]>depth[y]) swap(x,y);
	ans+=query(1,dfn[x],dfn[y])%p;
	return ans%p;
}
void Tree_son_update(int x,int k) //修改子树点权 
{
	//dfs序列连续 可以直接对子树操作 
	update(1,dfn[x],dfn[x]+sizes[x]-1,k);
	//节点x子树的dfs序区间为 dfn[x]~dfn[x]+sizes[x]-1 
}
int Tree_son_query(int x) //查询子树点权 操作同修改 
{
	return query(1,dfn[x],dfn[x]+sizes[x]-1)%p;
}
int main()
{
	memset(head,-1,sizeof(head));
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>root>>p;
	for(int i=1;i<=n;++i)
	{
		cin>>a[i];
	}
	for(int i=1;i<n;++i)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}
	dfs1(root,0,1);
	dfs2(root,root);
	//处理出重链来
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int op,x,y,z,res;
		cin>>op;
		if(op==1)
		{
			cin>>x>>y>>z;
			Tree_path_update(x,y,z%p);
		}
		else if(op==2)
		{
			cin>>x>>y;
			cout<<Tree_path_query(x,y)<<'\n';
		}
		else if(op==3)
		{
			cin>>x>>z;
			Tree_son_update(x,z%p);
		}
		else
		{
			cin>>x;
			cout<<Tree_son_query(x)<<'\n';
		}
	}
	return 0;
} 

END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值