【算法】重链剖分详解及模板

16 篇文章 0 订阅
6 篇文章 0 订阅
本文详细介绍了重链剖分这一数据结构,用于解决树上修改和查询问题。通过预处理,重链剖分能将复杂问题简化,用线段树等数据结构维护。文章解释了重链剖分的基本概念,如树链、重链、轻链,并给出了算法的实现思路和代码,包括初始化和查询修改操作。最后,通过例题进一步说明重链剖分的应用。
摘要由CSDN通过智能技术生成

前言

这是一篇蒟蒻的博客,可能有许多错误的地方,请大佬们指出。
先前总听说有一种神奇的算法名叫重链剖分,总想学习它,却一直抽不出时间,所以拖到现在才学了个大概。
好了,言归正传。下面就是我对重链剖分的一些理解。

什么是重链剖分

重链剖分是一种支持树上修改、查询等操作的数据结构,能够有效地维护树上的信息,将构造复杂的树分解成一条条链,从而使我们可以用数据结构(如平衡树线段树)维护这些信息。
例如有以下问题:

给定一棵有n个节点的树,一共进行m次操作。每一次操作都可以把任意一个一个节点的值进行更改,或者询问从节点u到v的最短路径上的最大值。

如果用暴力做这道题目,极限时间复杂度为 O ( n m ) O(nm) O(nm),显然TLE。
可以发现这里的操作都是线段树支持的,而重链剖分正是将这一复杂问题转化成可用线段树等强大的数据结构维护的问题的一大利器。

一些概念

在学习这个算法之前,我们需要明白以下的概念:

  1. 树链:指的是树上的路径(由一些连续的边组成)。
  2. 剖分:指的是把树链分成重链轻链两种。
  3. 重儿子:指的是所有子节点中子树节点数目最多的节点。
  4. 轻儿子:指的是子节点中除重儿子外其它的节点。
  5. 重边:指的是父节点和重儿子之间的连边。
  6. 轻边:指的是父节点和轻儿子之间的连边。
  7. 重链:指的是多条连续的重边连成的树链。
  8. 轻链:指的是多条连续的轻边连成的树链。
    这里写图片描述
    如上图,粗的边就是重边,细的边就是轻边
    算法中还定义了一些数组:
名称意义
size[i]以i为根的子树总节点数
fa[i]节点i的父节点
dep[i]节点i的深度
son[i]节点i的重儿子
top[i]节点i所在的重链的最顶上元素
pre[i]数据结构中位置为i的节点在原树中的对应位置
id[i]节点i在数据结构中的对应位置(与num[i]为互逆函数)

一些性质

1.如果(u,v)为轻边(u是v的父节点),则一定满足 s i z e v ≤ s i z e u 2 size_v\leq \frac{size_u}{2} sizev2sizeu
这个其实很容易理解。因为如果 s i z e v > s i z e u 2 size_v> \frac{size_u}{2} sizev>2sizeu的话,那么点v就比u的其它子节点加起来都要重,v就应当为u的重儿子。
2.从根结点到任意结点的路所经过的轻边、重链的条数必定都小于 l o g 2 n log_2n log2n

证明如下:
由于任一轻儿子对应的子树大小要小于父节点所对应子树大小的一半,因此从一个轻儿子沿轻边向上走到父节点后,所对应的子树大小至少变为两倍以上。经过的轻边条数自然是不超过 l o g 2 n log_2n log2n
然后由于重链都是间断的(连续的可以合成一条),所以经过的重链的条数是不超过轻边条数+1的,因此经过重链的条数也是 l o g log log级别的
综合可知原命题得证。

思路

我们可以先预处理出 s i z e i , f a i , s o n i , d e p i , t o p i , i d i , p r e i size_i,fa_i,son_i,dep_i,top_i,id_i,pre_i sizei,fai,soni,depi,topi,idi,prei的值,再利用 p r e i pre_i prei的值构造一颗线段树。要使得一条重链上的点在线段树上都是连续的一段,所以预处理要先处理重儿子,再处理轻儿子
接下来就到了处理操作了。

  • 对于修改操作(这里的只是单点修改),我们只需要把对应位置 i d i id_i idi的值在线段树中修改就可以了。
  • 对于询问操作,可以先求出(u,v)的最近公共祖先(以后简写成LCA),然后只要点u与LCA不在同一条重链上,我们就计算这一条重链的答案(在线段树中统计,因为一条重链上的点在线段树中的编号都是连续的,因此这只是相当于在线段树中进行一次区间查询),然后u就跳到 f a t o p u fa_{top_u} fatopu的位置,继续进行操作。当u和LCA都在一条重链上时,直接在线段树中统计u到LCA之间的答案即可(因为它们间的点一定都是连续的)。记得把点v也进行一次这样的操作。

最后,我们要把答案减去 a n s [ L C A ] ans[LCA] ans[LCA](因为这个点在答案中已经被计算过两次)


实现

重链剖分的实现过程大体可以分成以下两步:

  1. 初始化
  2. 查询及修改

初始化

我们需要遍历整棵树两次,再遍历整棵线段树

DFS1

在这一遍的DFS中,我们只需要初始化 s i z e i , f a i , s o n i , d e p i size_i,fa_i,son_i,dep_i sizei,fai,soni,depi的值。由于我求LCA时用的是倍增法,所以我这里用 f i , 0 f_{i,0} fi,0表示 f a i fa_i fai
代码如下:

void dfs1(ll k,ll from)//size[i],f[i][0],son[i][0],dep[i][0]
{
	dep[k]=dep[from]+1,f[k][0]=from,size[k]=1;
	for(int i=first[k];i;i=a[i].next) if(a[i].end!=from)
	{
		dfs1(a[i].end,k);
		size[k]+=size[a[i].end];
		if(size[son[k]]<size[a[i].end]) son[k]=a[i].end;
	}
}

DFS2

我们要用这一个DFS初始化出 t o p i , i d i , p r e i top_i,id_i,pre_i topi,idi,prei的值。由于我们要使一条重链上的所有点都是线段树上连续的一段,因此我们要先DFS重儿子,再DFS轻儿子
那么如何求 t o p i top_i topi呢?这里可以分成两种情况:

  1. 当i为它父节点的重儿子时, t o p i = t o p f a i top_i=top_{fa_i} topi=topfai
  2. 否则, t o p i = i top_i=i topi=i
    这个其实很容易理解——由于每一个点都一定在一条重链上,所以它要么是这条重链的最顶上节点,要么是这条重链下的一个节点。
    代码如下:
void dfs2(ll k,ll from)//id[i] pre[i] top[i] val[i]
{
	pre[id[k]=(++s)]=k;
	top[k]=from;
	if(son[k]) dfs2(son[k],from);//先走重儿子
	for(int i=first[k];i;i=a[i].next)
		if(a[i].end!=f[k][0]&&a[i].end!=son[k])
			dfs2(a[i].end,a[i].end);//不能重复走重儿子,也不能走回父节点
}

最后只用初始化一下线段树就可以了。


查询及修改

修改操作

直接修改线段树上对应的节点。

void change(ll k,ll l,ll r,ll num,ll v)//将点num的值修改成v
{
	if(l==r)
	{
		tree[k].max=tree[k].sum=v;
		return;
	}
	ll mid=(l+r)/2;
	if(num<=mid) change(k*2,l,mid,num,v);
	else change(k*2+1,mid+1,r,num,v);
	update(k);//更新点k的信息
}

查询操作

求路径上的点权之和:

inline ll get_sum(ll x,ll y)//求x到y的最短路径上的点权之和
{
	ll res=0,i=2,z=lca(x,y);//z表示x和y的LCA
	while(i--)//一共要操作2次
	{
		while(top[x]!=top[z])
		{
			res+=find_sum(1,1,n,id[top[x]],id[x]);
			x=f[top[x]][0];
		}
		res+=find_sum(1,1,n,id[z],id[x]);//求线段树上id[z]到id[x]的和
		x=y;//将点x换成点y,继续操作,这里是为了压缩代码
	}
	return res-w[z];
	//w[z]为lca到根的值,这里是要减去重复计算的w[z](它在计算点x,点v时都被重复地计算)
}

求路径上的最大值其实也是相似的,这里就不写了。


总结

其实重链剖分并不难,画一下图就可以理解。但是它的码量却出奇的长(搞不好会突破200行)。调试时要有耐心、有毅力,实在不行把整个函数重打也可以。重链剖分还可以套其它的数据结构,如Splay,树状数组等,有兴趣的可以自己打一下。


例题

这里放一道例题帮助理解:

【ZJOI2008】树的统计

Description

一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w。
  我们将以下面的形式来要求你对这棵树完成一些操作:
  I. CHANGE u t : 把结点u的权值改为t
  II. QMAX u v: 询问从点u到点v的路径上的节点的最大权值
  III. QSUM u v: 询问从点u到点v的路径上的节点的权值和
  注意:从点u到点v的路径上的节点包括u和v本身

Input

输入文件的第一行为一个整数n,表示节点的个数。
  接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。
  接下来n行,每行一个整数,第i行的整数wi表示节点i的权值。
  接下来1行,为一个整数q,表示操作的总数。
  接下来q行,每行一个操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。

Output

对于每个“QMAX”或者“QSUM”的操作,每行输出一个整数表示要求输出的结果。

Sample Input

4
1 2
2 3
4 1
4 2 1 3
12
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4

Sample Output

4
1
2
2
10
6
5
6
5
16

Data Constraint

对于100%的数据,保证1<=n<=30000,0<=q<=200000;中途操作中保证每个节点的权值w在-30000到30000之间。

代码

#include<cstdio>
using namespace std;
#define ll long long
#define N 30010
struct edge{ll end,next;}a[N<<1];
struct TREE{ll sum,max;}tree[N*15];
ll first[N],id[N],val[N],dep[N],top[N],son[N],size[N],f[N][16],w[N],n,m,s;
inline ll max(ll x,ll y){return x>y?x:y;}
inline void swap(ll &x,ll &y){ll z=x;x=y;y=z;}
//construct
inline void inc(ll x,ll y)//建边
{
	a[++s]=(edge){y,first[x]},first[x]=s;
	a[++s]=(edge){x,first[y]},first[y]=s;
}
inline void update(ll k)//更新线段树中k的值
{
	tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;
	tree[k].max=max(tree[k*2].max,tree[k*2+1].max);
}
void dfs1(ll k,ll from)//f[i][0]--father son[i] size[i] dep[i]--id in TREE
{
	dep[k]=dep[from]+1,f[k][0]=from,size[k]=1;
	for(int i=first[k];i;i=a[i].next) if(a[i].end!=from)
	{
		dfs1(a[i].end,k);
		size[k]+=size[a[i].end];
		if(size[son[k]]<size[a[i].end]) son[k]=a[i].end;
	}
}
void dfs2(ll k,ll from)//id[i] pre[i] top[i] val[i]
{
	val[id[k]=(++s)]=w[k];
	top[k]=from;
	if(son[k]) dfs2(son[k],from);
	for(int i=first[k];i;i=a[i].next)
		if(a[i].end!=f[k][0]&&a[i].end!=son[k])
			dfs2(a[i].end,a[i].end);
}
void makeTree(ll k,ll l,ll r)//build the TREE
{
	if(l==r)
	{
		tree[k].max=tree[k].sum=val[l];
		return;
	}
	ll mid=(l+r)/2;
	makeTree(k*2,l,mid);
	makeTree(k*2+1,mid+1,r);
	update(k);
}
//ask&change
void change(ll k,ll l,ll r,ll num,ll v)//将num的值修改成v
{
	if(l==r)
	{
		tree[k].max=tree[k].sum=v;
		return;
	}
	ll mid=(l+r)/2;
	if(num<=mid) change(k*2,l,mid,num,v);
	else change(k*2+1,mid+1,r,num,v);
	update(k);
}
inline ll lca(ll u,ll v)//找u和v的LCA
{
	ll i,j;
	if(dep[u]<dep[v]) swap(u,v);
	for(i=15;i>=0;i--)
		if(dep[f[u][i]]>=dep[v])
			u=f[u][i];
	if(u==v) return u;
	for(i=15;i>=0;i--)
		if(f[u][i]!=f[v][i])
			u=f[u][i],v=f[v][i];
	return f[u][0];
}
ll find_max(ll k,ll l,ll r,ll x,ll y)//求线段上x到y的最大值
{
	if(l==x&&r==y){return tree[k].max;}
	ll mid=(l+r)/2;
	if(y<=mid) return find_max(k*2,l,mid,x,y);
	else if(x>mid) return find_max(k*2+1,mid+1,r,x,y);
	return max(find_max(k*2,l,mid,x,mid),find_max(k*2+1,mid+1,r,mid+1,y));
}
ll find_sum(ll k,ll l,ll r,ll x,ll y)//求线段树上从x到y的和
{
	if(l==x&&r==y){return tree[k].sum;}
	ll mid=(l+r)/2;
	if(y<=mid) return find_sum(k*2,l,mid,x,y);
	else if(x>mid) return find_sum(k*2+1,mid+1,r,x,y);
	return find_sum(k*2,l,mid,x,mid)+find_sum(k*2+1,mid+1,r,mid+1,y);
}
inline ll get_max(ll x,ll y)//x到y求最大值
{
	ll res=-30000,i=2,z=lca(x,y);
	while(i--)
	{
		while(top[x]!=top[z])
		{
			res=max(res,find_max(1,1,n,id[top[x]],id[x]));
			x=f[top[x]][0];
		}
		res=max(res,find_max(1,1,n,id[z],id[x]));
		x=y;
	}
	return res;
}
inline ll get_sum(ll x,ll y)//x到y求和
{
	ll res=0,i=2,z=lca(x,y);
	while(i--)
	{
		while(top[x]!=top[z])
		{
			res+=find_sum(1,1,n,id[top[x]],id[x]);
			x=f[top[x]][0];
		}
		res+=find_sum(1,1,n,id[z],id[x]);
		x=y;
	}
	return res-w[z];
}
//main
int main()
{
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
	ll i,j,x,y,z;char ch;
	scanf("%lld",&n);
	for(i=1;i<n;i++)
	{
		scanf("%lld%lld",&x,&y);
		inc(x,y);
	}
	for(i=1;i<=n;i++) scanf("%lld",&w[i]);
	s=0,dfs1(1,0),dfs2(1,0);
	makeTree(1,1,n);
	scanf("%lld\n",&m);
	for(j=1;j<16;j++)
		for(i=1;i<=n;i++)
			f[i][j]=f[f[i][j-1]][j-1];
	for(i=1;i<=m;i++)
	{
		ch=getchar();
		if(ch=='C')
		{
			scanf("HANGE %lld %lld\n",&x,&y);
			w[x]=y,change(1,1,n,id[x],y);
		}
		else
		{
			ch=getchar();
			if(ch=='M')
			{
				scanf("AX %lld %lld\n",&x,&y);
				printf("%lld\n",get_max(x,y));
			}
			else
			{
				scanf("UM %lld %lld\n",&x,&y);
				printf("%lld\n",get_sum(x,y));
			}
		}
	}
	return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值