【谈谈知识点】重链剖分

本文详细介绍了树链剖分这一数据结构算法,用于解决树上的动态区间加减问题。通过重链剖分,将树转化为一系列连续的链,结合线段树和LCA(最近公共祖先)算法,实现O(logn)的复杂度。内容包括重链剖分的定义、性质、复杂度分析、模板代码以及应用实例,还提及了换根操作的处理方法。
摘要由CSDN通过智能技术生成

前言

好久没写博客了上来水一发
当年调板子的时候调到天昏地暗头脑发晕
但是写顺手之后太~ 爽 ~啦!(x
然而光是求lca还是建议倍增,树剖常数太大哩(悲

Part 0 引入

对于序列上的区间加减,大家想必有很多解决的方式了。静态可以通过预处理、动态规划等降低复杂度,动态往往使用数据结构进行维护,让每次询问都有相对优秀的复杂度。
但如果是在树上呢?
先考虑静态,那我们同样有不少方法,树形dp,树上差分,倍增,etc.
但如果是动态,我们需要一种算法去适应树的结构,就如我们用线段树等去匹配序列的结构一样,从而实现子树||链上的修改和查询。
所以今天我们要讨论的是树上动态问题中一个常用的手段——重链剖分。

Part 1 定义

树链剖分其实算是一大类算法的集合,其中包括但不限于重链剖分,长链剖分,LCT等用于树上动态问题的算法。但一般而言,重链剖分是用的最多也最广泛的,所以后来树链剖分和重链剖分概念上就基本等价了
(什么公车私用剧情

那么就重链剖分而言,完全可以采用语文阅读理解的方式。
第二个字——链,后两个字——剖分,实际上是在进行对算法的描述。也就是说,这种算法的本质是把一棵树通过某种方式,剖分成一条一条的链,再对每条链(也就是序列)采用已知算法来求解。
将树上问题转化为序列问题
很明显的,如果我们随意进行剖分,可能会出现链数量过多的问题,从而无法保证复杂度。因此大佬们发明了不少保证复杂度的剖分方式。

那么第一个字——重,自然是在阐述这种算法的特别之处了。
下面开始定义:

对于树上某一节点:
重儿子: 所有子节点中子树最大的被称为重儿子。
轻儿子: 除重儿子之外的子节点。
重边:从该结点到重子节点的边为
轻边:~~~~
重链:若干条首尾衔接的重边构成重链

附图(图源为他人博客,侵删)
在这里插入图片描述

可以看到,以重儿子的原则进行dfs并进行编号后,整棵树被划分成了若干DFN序连续的链条,只需要把他们按DFN序排列后(想象一下拆成链条,序号连续处把链条首尾相接绑起来,然后拉成一串,锵锵!)就是一段连续的序列啦~

这下就可以召唤线段树了(

那么按上文方式定义之后,我们可以得到两个性质:

(1)轻边(u,v)中,size(v)<=size(u/2)
解释:否则它就是重儿子了
(2)从根到某一点的路径上,不超过logn条轻边和不超过logn条重链。
解释:由性质1,轻儿子为根的子树的大小必然不超过原树大小的1/2,因此每经过一条轻边,树的大小至少减小一半,因此最多logn条轻边
而每走一条轻边就相当于从一条重链跳到另一条重链上,因此最多logn条重链

Part 1.5 复杂度

那么有人就要问了: 复杂度是怎么保证的呢?
答:对于每一次查询,我们把查询的链分在我们划分的多条链上,分别进行线段树修改/查询。 线段树修改查询操作的时间复杂度是 O(logn);而在树链上每走一条轻边,子树大小就 /= 2,所以最多走 logn 条轻边,处理logn条重链,复杂度 O(logn)。

嗯,这样我们就得到了一个优秀的O(logn · logn)
(而且这两个logn基本上不满

Part 2 模板

重链剖分的代码可以概括为:DFS+线段树+LCA
DFS是为了处理信息,线段树和LCA是为了解决问题
下面一步一步来讲:

①DFS(两遍)

通过定义部分我们能知道,树链剖分的前置工作非常非常多(),要处理重儿子和重链(也就说明要子树大小,链上信息等),要处理树上编号和线段树编号(即DFN序)的对应关系,还要处理LCA需要用到的深度等……阿巴阿巴

一般而言我们分为两次DFS。第一次,我们处理出重儿子和其他易于统计的信息:

//son[x]:x的重儿子的编号
inline void dfs1(int n)
{
	size[n]=1;
	for(int i=head[n],v;;i=e[i].nxt){
		v=e[i].v;
		if(v==fa[n])continue;
		fa[v]=n,deep[v]=deep[n]+1;
		dfs1(v);size[n]+=size[v];
		if(size[v]>=size[son[n]])//更新重儿子
		son[n]=v;
	}
}

第二次,我们按照重儿子优先的原则去dfs并记下DFN序和节点编号的对应关系,顺便处理链上信息。

//top[n]:n所在重链的链首的编号
//id[n]:节点n的DFN序
//rank[n]:DFN序为n的节点的编号
//op:链首编号(原来我也玩___)
inline void dfs2(int n,int op)
{
	id[n]=++cnt;top[n]=op;rank[cnt]=n;
	if(!son[n])continue;//没重儿子就是到叶节点了
	dfs2(son[n],op);//走重儿子,重链连续,链首不变
	for(int i=head[n],v;;i=e[i].nxt){
		v=e[i].v;
		if(v==fa[n]||v==son[n])continue;
		dfs2(v,v)//过轻边到轻儿子,即另一条重链的链首
	}
}
②线段树

至此我们就处理完了所有需要的信息,成功剖树为链~
接下来老规矩上线段树即可,没啥可讲的

//需要进行的操作依题目而变,所以只写一点点代码
inline void build(int l,int r,int n)
{
	t[n].l=l,t[n].r=r;
	if(l==r){
		t[n].val=val[rank[l]];
		//这个映射可以思考一下
		return;
	}
	int mid=(l+r)/2;
	build(l,mid,n<<1);
	build(mid+1,r,(n<<1)|1);
	update(n);
}
③LCA

树链剖分中找LCA遵循一下过程:(以x,y为例)
1、当x,y不在同一条重链,比较x和y所在重链链首的深度大小
2、较深的那个沿着重链跳到链首的父亲处,重复1
3、当x,y在同一条重链,深度小的为LCA

因为每次“跳”必定经过轻边,复杂度同上文一样有保证
之所以比较链首深度而非节点本身的深度,是担心出现跳过了的情况,比如下图:
在这里插入图片描述
(太丑了,简直丑贯天灵
对于节点A,B而言深度相同,如果此时选择先跳B,就会直接跳到根节点,就寄了(
比较链首深度就不会出现这个情况~

代码如下:

//找LCA的代码
inline int Lca_Place(int x,int y)
{

	while(top[x]!=top[y]){
	    if(deep[top[x]]<deep[top[y]])swap(x,y);
	    x=fa[top[x]];
    }
	return (deep[x]>deep[y])?(y):(x);
} 

//x到y路径操作的代码,操作就是自己线段树对应的那部分
//这里写的加,ql和qr是写来好看的
ivoid Lca_Add(int x,int y)
{	

	while(top[x]!=top[y]){
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		ql=id[top[x]],qr=id[x];//x到链首进行加
		addsome(ql,qr,1);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	ql=x,qr=y,addsome(ql,qr,1);
}

Part 3 水题

[JLOI2014] 松鼠的新家:链加
[NOI2015]软件包管理器:链查询/修改,子树查询/修改
[USACO15DEC]Max Flow P 链加
[ZJOI2008]树的统计 链加+链极值
……(挺多的)

Ex 换根

一点点小的扩展芝士,虽然还没遇见过能用的地方(
我们要支持这样一个操作:将一个指定的节点设置为树的新根。
首先很明显的,我们不可能换一次根就重新剖一次树,因此我们考虑换根之后答案发生什么变化:
链上的修改无所谓,因为换根不会影响路径,唯一要考虑的就是对子树答案的影响。
考虑我们现在的树根是root,当前询问的子树根节点是u。 那么会有以下情况(以下LCA均指原树中的LCA)
1.u==root,那么u的子树就是整棵树。
2.LCA(root,u)≠u,即root不在u的子树中。那么u现在的子树就是原来的子树
3.LCA(root,u)=u,即u在原来的树中是root的祖先。那么我们找到u到root路径上的第一个儿子。这个儿子对应的原树中的子树,就是现在u的子树的补集。
分别对应转换一下就好了。实际上有点像天平向左向右倾斜的变化,手动画个图就ok~

Part 4 总结

总的思想就是用链维护树,用线段树维护链
要注意的细节非常之多,而且线段树一定是基础,不能出错!否则调代码调死你,鬼知道错的是dfs1,dfs2,线段树还是lca(
那么最后感谢阅读本文~
有空可以来私信催稿避免我摆烂 ,尽量保证一周五篇起(当然不一定是题解

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值