轻重链剖分

树链剖分

树链剖分是一种将树转化为一条链的算法,通常和线段树,树状数组,DP等针对链的算法结合使用。

正如题目所说,本文只讲轻重链剖分

轻重链剖分

概念定义

  • 重儿子/轻儿子

    求出每棵子树的大小,节点数最多的一个子树的根节点就是这个节点父节点的重儿子。

    轻儿子即是节点个数最少的子树的根节点。

  • 重边/轻边

    重边即是连接重儿子与其父亲的边。

    轻边即是连接轻儿子与其父亲的边。

  • 重链/轻链

    重链是由重边组成的极大路径

    轻链是由轻边组成的极大路径

    单独一个点也可以作为一个特殊的重链/轻链存在,所以对于任意一个节点,都一定在一个重链里面


做法

预处理

首先扔出一个定理:

树中任意一条路径均可以拆分成一条链上 \(O(log\ n)\) 个连续区间。

证明+构造:一个DFS序就行了

但是实际代码实现中,一般都是优先遍历重儿子,这样遍历过后,可以保证重链的编 号是连续的。

这个性质尤其有用。

有了这种DFS序,我们可以让上面的那个定理更确切一点:

树中的每一条路径,都可以拆分为 \(O(log\ n )\) 条重链(重链可能不完整),即可拆分为 \(O(log\ n)\) 个连续区间

所以首先我们要做的是两个DFS对树进行预处理

第一次DFS:找到重儿子

第二次DFS:建立DFS序,根据第一次找到的重儿子找重链

void dfs1(int x,int f)	//寻找重儿子 
{
	dpt[x]=dpt[f]+1; //记录深度 
	fa[x]=f; //记录父节点 
	size_[x]=1;	//子树至少有他自己 
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(y==f) continue;
		dfs1(y,x);
		size_[x]+=size_[y];		//子树大小加起来 
		if(size_[son[x]]<size_[y]) son[x]=y;	//打擂台记录重儿子 
	}
}

void dfs2(int x,int t)	//t:当前重链的起点是谁 
{
	id[x]=++cnt; //记录DFS序 
	nwpoi[cnt]=poi[x]; //节点的权值重新排序 
	top[x]=t; //记录这个节点所在重链的起点 
	if(!son[x]) return ;
	dfs2(son[x],t);	//优先搜索重儿子,重儿子一定在当前这条重链内 
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(y==fa[x]||y==son[x]) continue;
		dfs2(y,y);	//轻儿子一定是另外一条重链的开头 
	}
} 

处理完过后, \(nwpoi\) 数组就是DFS序下的点权数组, \(id\) 数组就是每个点在DFS下对应的编号, \(top\) 就是每个节点所在重链的起始节点

查询与操作:

那么现在来以对于树的四种个基本操作为例子,试着初步运用我们所得到的信息

  • 1:求以 \(x\) 为根节点的子树内所有节点值之和

  • 2:将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)

  • 3: 将树从 \(x\)\(y\) 结点最短路径上所有节点的值都加上 \(z\)

  • 4: 求树从 \(x\)\(y\) 结点最短路径上所有节点的值之和。

其实就是P3384树链剖分

这四种操作关键在于两个点:如何找到覆盖一个节点的子树的所有区间如何找到覆盖一条路径的所有区间。找到之后就可以用线段树或树状数组来操作,程序中使用的是线段树。

  • 子树查找

一个节点的子树很简单,它在DFS序中是连续的一段,长度就是其子树的大小。

完成这个过程的函数如下:

void update_tree(int x,int k)
{
	int l=id[x],r=id[x]+size_[x]-1;	//子树的dfs序是连续的一段 
	update(1,1,n,l,r,k);
}

ll query_tree(int x)
{
	int l=id[x],r=id[x]+size_[x]-1;
	return query(1,1,n,l,r);
}
  • 路径查找

那路径呢?

考虑一下LCA。

遵循从特殊到一般的原则,先想想路径两端点若是在同一个重链中会怎样。

由于我们在搜索时令重链编号连续,所以它们是一个连续区间,直接线段树就好了。

如果在不同的重链中呢?

我们就要去看深度最深的那个节点的重链起点,显然它的重链起点和另外一个节点一开始也不应该是相等的。于是我们继续看它的重链起点的重链起点......直到它们重链起点相同,也就是在同一重链时为止。

这是一个类似于倍增LCA往上跳的过程,但这里每次跳跃到的是当前重链的起点。每跳一次做一次线段树。

完成这个过程的函数如下:

void update_path(int x,int y,int k)
{
	while(top[x]!=top[y])	//如果不在同一重链 
	{
		if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
		update(1,1,n,id[top[x]],id[x],k);
		x=fa[top[x]];	//向上跳,直到位于同一重链 
	}
	if(dpt[x]<dpt[y]) swap(x,y);
	update(1,1,n,id[y],id[x],k);
}

ll query_path(int x,int y)
{
	ll res=0;
	while(top[x]!=top[y])
	{
		if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
		res+=query(1,1,n,id[top[x]],id[x]);
		x=fa[top[x]];
	}
	if(dpt[x]<dpt[y]) swap(x,y);
	res+=query(1,1,n,id[y],id[x]);
	return res;
}

以上就是轻重链剖分的基本操作了。

时间复杂度

预处理 \(O(n)\)

处理一条路径 \(O(log^2n)\)

处理一棵子树 \(O(log n)\)

完整代码

P3384树链剖分

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; 

const int N=2e6+10,M=N<<1;

int head[N],ver[M],nxt[M],tot=0;
void add(int x,int y)
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}
int n,m;
int poi[N];
int id[N],nwpoi[N],cnt=0;
int dpt[N],size_[N],top[N];	//每个点的深度,每个点为根的子树大小,所在重链的顶点

int fa[N],son[N];//父节点,重儿子 

ll tree[N<<2],lazy[N<<2];//线段树相关 

void dfs1(int x,int f)	//寻找重儿子 
{
	dpt[x]=dpt[f]+1; //记录深度 
	fa[x]=f; //记录父节点 
	size_[x]=1;	//子树至少有他自己 
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(y==f) continue;
		dfs1(y,x);
		size_[x]+=size_[y];		//子树大小加起来 
		if(size_[son[x]]<size_[y]) son[x]=y;	//打擂台记录重儿子 
	}
}

void dfs2(int x,int t)	//t:当前重链的起点是谁 
{
	id[x]=++cnt; //记录DFS序 
	nwpoi[cnt]=poi[x]; //节点的权值重新排序 
	top[x]=t; //记录这个节点所在重链的起点 
	if(!son[x]) return ;
	dfs2(son[x],t);	//优先搜索重儿子,重儿子一定在当前这条重链内 
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(y==fa[x]||y==son[x]) continue;
		dfs2(y,y);	//轻儿子一定是另外一条重链的开头 
	}
} 
 
inline void push_up(int node)
{
	tree[node]=tree[node<<1]+tree[node<<1|1];
}

void func(int node,int start,int end,int k)
{
	lazy[node]=lazy[node]+k;
	tree[node]=tree[node]+k*(end-start+1);
}
void push_down(int node,int start,int end)
{
	if(lazy[node]==0) return;
	ll mid=start+end>>1;
	int lnode=node<<1;
	int rnode=node<<1|1;
	func(lnode,start,mid,lazy[node]);
	func(rnode,mid+1,end,lazy[node]);
	lazy[node]=0;
}

void build(int node,int start,int end)
{
	lazy[node]=0;
	if(start==end)
	{
		tree[node]=nwpoi[start];
		return ;
	}
	int mid=start+end>>1;
	int lnode=node<<1;
	int rnode=node<<1|1;
	build(lnode,start,mid);
	build(rnode,mid+1,end);
	
	push_up(node);
}

void update(int node,int start,int end,int l,int r,int val)
{
	if(l<=start&&end<=r)
	{
		tree[node]+=val*(end-start+1);
		lazy[node]+=val;
		return ;
	}
	push_down(node,start,end);
	
	int mid=start+end>>1;
	int lnode=node<<1;
	int rnode=node<<1|1;
	
	if(l<=mid) update(lnode,start,mid,l,r,val);
	if(r>mid) update(rnode,mid+1,end,l,r,val);
	
	push_up(node);
}

ll query(int node,int start,int end,int l,int r)
{
	if(end<l||start>r) return 0;
	if(l<=start&&end<=r)
		return tree[node];
		
	push_down(node,start,end);
	
	int mid=start+end>>1;
	int lnode=node<<1;
	int rnode=node<<1|1;
	ll lsum=query(lnode,start,mid,l,r);
	ll rsum=query(rnode,mid+1,end,l,r);
	return lsum+rsum;
	
	push_up(node);
}

void update_path(int x,int y,int k)
{
	while(top[x]!=top[y])	//如果不在同一重链 
	{
		if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
		update(1,1,n,id[top[x]],id[x],k);
		x=fa[top[x]];	//向上跳,直到位于同一重链 
	}
	if(dpt[x]<dpt[y]) swap(x,y);
	update(1,1,n,id[y],id[x],k);
}

ll query_path(int x,int y)
{
	ll res=0;
	while(top[x]!=top[y])
	{
		if(dpt[top[x]]<dpt[top[y]]) swap(x,y);
		res+=query(1,1,n,id[top[x]],id[x]);
		x=fa[top[x]];
	}
	if(dpt[x]<dpt[y]) swap(x,y);
	res+=query(1,1,n,id[y],id[x]);
	return res;
}

void update_tree(int x,int k)
{
	int l=id[x],r=id[x]+size_[x]-1;	//子树的dfs序是连续的一段 
	update(1,1,n,l,r,k);
}

ll query_tree(int x)
{
	int l=id[x],r=id[x]+size_[x]-1;
	return query(1,1,n,l,r);
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",poi+i);
	for(int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	
	dfs1(1,1);
	dfs2(1,1);
	build(1,1,n);
	
	scanf("%d",&m);
	while(m--)
	{
		int k,x,y,val;
		scanf("%d%d",&k,&x);
		if(k==1)
		{
			scanf("%d%d",&y,&val);
			update_path(x,y,val);	//修改路径上的节点权值 
		}
		else if(k==2)
		{
			scanf("%d",&val);
			update_tree(x,val);		//修改子树上的节点权值 
		}
		else if(k==3)
		{
			scanf("%d",&y);
			ll ans=query_path(x,y); 	//询问路径权值和
			printf("%lld\n",ans); 
		}
		else if(k==4)
		{
			ll ans=query_tree(x);		//询问子树权值和 
			printf("%lld\n",ans);
		}
	}
	return 0; 
}

总结

树链剖分是一种优秀的算法。完美地体现了化繁为简的思想,为许多树上操作提供了便利。树剖码量很大,刚学完时调代码可能会很难受。但是熟练之后错误率就小很多了。

posted @ 2020-11-01 21:49  RemilaScarlet  阅读(482)  评论(0)    收藏  举报