树链剖分

前言

以下内容大多摘抄自董晓算法

树链剖分(轻重链剖分)

前置芝士

相关定义

重儿子:父节点的所有儿子中子树结点数目最多的结点

轻儿子:父节点中除了重儿子之外的儿子

重边:父结点和重儿子连成的边

轻边:父结点和轻儿子连成的边

重链:由多条重边连接而成的路径

前置数组含义

\(fa[u]\):存 \(u\) 的父节点

\(dep[u]\):存 \(u\) 的深度

\(son[u]\):存 \(u\) 的重儿子

\(size[u]\):存以 \(u\) 为根的子树的结点数

\(top[u]\):存 \(u\) 所在的重链的顶点(深度最小的那个点)

\(id[u]\):存 \(u\) 剖分后的新编号

\(wt[tot]\):存新编号在树中所对应结点的权值。

相关性质

  • 整棵树会被剖分成若干条重链

  • 轻儿子一定是每条重链的顶点

  • 任意一条路径被切分成不超过 \(logn\) 条链

树链剖分可以解决的问题

树链剖分可以解决树上的一些链操作,比如将 \(u\)\(v\) 路径上的权值做一些操作、对 \(u\) 的子树做一些操作、求 LCA 等一些比较繁琐的操作。

算法原理

树链剖分是基于 dfs 来实现的,原理比较简单。

如果是处理 \(u\) 的子树方面的问题,那么由于 dfs 的性质,一个子树内的 dfs 顺序是连续的,所以我们可以很简单的进行维护;

如果要维护一条链呢?我们并不能保证这条链上结点是连续的,那就退而求其次。由树剖的性质可得,任意一条路径会被切分成不超过 \(logn\) 条链,因此我们分别维护每一条链就行了。

怎么快速地维护呢?线段树/oh。因为我们要维护每一条重链,所以我们在 dfs 的时候优先 dfs 重儿子,使得一条链上的结点的 dfs 序连续,这样我们就能把树上的每一条链转换为一个一个的小区间,而每一个小区间就可以线段树进行维护了。

总之,树链剖分就是把树转换成一个序列,再利用线段树进行维护,就能高效的处理树上问题了。

一般的时间复杂度是 \(O(nlog^2n)\) 线段树的 \(log\) 和树剖跳链的 \(log\)

算法流程

一.预处理

第一次 dfs ,搞出 \(fa,dep,size,son\) 数组(为了剖分做准备)

第二次 dfs ,搞出 \(top,id,wt\) 数组

il void dfs1(int x,int f)
{
	dep[x] = dep[f] + 1 , fa[x] = f , siz[x] = 1;
	for(re int i=head[x];i;i=edge[i].next)
	{
		int y = edge[i].v;
		if(y == f) continue;
		dfs1(y,x);
		siz[x] += siz[y];
		if(siz[son[x]] < siz[y]) son[x] = y;
	}
}

il void dfs2(int x,int topf)
{
	id[x] = ++tot , wt[tot] = w[x] , top[x] = topf;
	if(!son[x]) return ;
	dfs2(son[x],topf);
	for(re int i=head[x];i;i=edge[i].next)
	{
		int y = edge[i].v;
		if(y == fa[x] || y == son[x]) continue;
		dfs2(y,y);
	}
}

利用新 \(id\)\(wt\) 建树

il void build(int p,int l,int r)
{
	if(l == r)
	{
		tree[p] = wt[l] % mod;
		return ;
	}
	int mid = (l+r) >> 1;
	build(lc,l,mid);
	build(rc,mid+1,r);
	tree[p] = (tree[lc] + tree[rc]) % mod;
}

二.查询

1.查询树从 \(x\)\(y\) 结点最短路径上所有节点值的和:

il int Query(int nl,int nr,int l,int r,int p)
{
	int res = 0;
	if(l >= nl && r <= nr) return tree[p] % mod;
	push_down(p,l,r);
	int mid = (l+r) >> 1;
	if(nl <= mid) res += Query(nl,nr,l,mid,lc);
	if(nr > mid) res += Query(nl,nr,mid+1,r,rc);
	return res % mod;
}
      
il int Query_way(int x,int y)
{
	int res = 0;
	while(top[x] != top[y])
	{//不断的往上跳
		if(dep[top[x]] < dep[top[y]]) swap(x,y);
		res = (res + Query(id[top[x]],id[x],1,n,1)) % mod;
		x = fa[top[x]];//这条链处理完了,跳到另一条链上
	}
	if(dep[x] > dep[y]) swap(x,y);
	res = (res + Query(id[x],id[y],1,n,1)) % mod;//x,y在同一条链上了,再把它俩之间的部分处理一下
	return res % mod;
}

2.将 \(x\)\(y\) 结点上的权值增加 \(k\)

il void Modify(int nl,int nr,int l,int r,int p,int k)
{
	if(l >= nl && r <= nr)
	{
		tag[p] = (tag[p]+k) % mod;
		tree[p] = (tree[p]+(r-l+1)*k) % mod;
		return ;
	}
	push_down(p,l,r);
	int mid = (l+r) >> 1;
	if(nl <= mid) Modify(nl,nr,l,mid,lc,k);
	if(nr > mid) Modify(nl,nr,mid+1,r,rc,k);
	tree[p] = (tree[lc] + tree[rc]) % mod;
}//其实这部分就是普通的线段树
      
 il void Modify_way(int x,int y,int k)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) swap(x,y);
		Modify(id[top[x]],id[x],1,n,1,k);
		x = fa[top[x]];
	}//原理其实同 Query_way 相似
	if(dep[x] > dep[y]) swap(x,y);
	Modify(id[x],id[y],1,n,1,k);
}

说白了,树链剖分只是充当了一个划分序列的角色,真正的大头还是线段树。

3.查询 \(u\) 及其子树的权值和

因为我们知道,一个点子树的 dfs 序是连续的,因此剖分后的区间的左端点就是 \(id[u]\) ,右端点就是 \(id[u]+siz[u]-1\) ,我们直接 Query 即可

Query(id[x],id[x]+siz[x]-1,1,n,1)

4.将 \(u\) 及其子树的权值加 \(k\)

原理同上

Modify(id[x],id[x]+siz[x]-1,1,n,1,k);

总结

树链剖分其实只是一个工具,真正树剖题变化的还是你的线段树想维护什么:区间最大值,区间和,区间变化,树剖+二分等等很多的变化形式,做题多了就能见识到了,上文所提及的都是最基础的一部分。

posted @ 2023-05-25 20:25  Bloodstalk  阅读(15)  评论(0编辑  收藏  举报