树剖笔记

树剖是一种解决树上问题的思想。重链剖分一般就是所说的树链剖分,简称树剖

先思考问题一:如何实现查询树上两点间最短路径代价和?

容易发现可以维护所有点到根节点的距离和,再用倍增求 lca。不妨记两点分别为 \(x,y\),到根节点的距离为 \(dis\),即答案为 \(dis_x+dis_y-2\times dis_{lca(x,y)}\)

问题二:如何实现树上区间加?

更容易地,我们能想到做树上差分,能够做到 \(O(1)\) 的修改。不做过多赘述。

拼起来?

这时会发现不太好做了,查询的话需要还原整个差分树,复杂度很高,不能接受。

真正优秀的算法是基于暴力的,我们思考暴力。

对于问题一,暴力的做法仍是向上跳,设 \(x\) 的深度大于 \(y\)\(x\) 一直向父亲节点跳,直到 \(x,y\) 存在于同一条链上,计算 \(x,y\) 之间的权值即可。至于如何计算权值,待会再讲。

对于问题二,那就是直接暴力扫了,不过多赘述。

树剖正是这种暴力的优化。

在介绍树剖之前,我们将不可避免地接触一些名词:

重子:设有一个节点,其所有子节点中,以子节点为根的子树节点数最大的那个子节点即为该节点的重子。

轻子:设有一个节点,其所有节点中,除重子外的节点均为轻子。

重边:某个节点与其重子的连边。

轻边:某个节点与其轻子的连边。

重链:仅由重边组成的一条链。

轻链:仅由轻边组成的一条链。

特别的,若某个点有多个重子,任选其一;若某个点不存在子节点,则不存在重子和轻子。


树剖本质上就是一种对暴力的优化,它将维护树上的一些条链各种信息。

以上面的问题为例。

我们先对每一个节点做编号,以重子优先做 dfs,新的节点编号即为 dfn。这样就可以保证重链上节点编号连续了。

当我们往上跳的时候,可以直接跳过所在重链,跳过的这一段正好编号连续,随便拿个数据结构就能维护。

操作时,若两点不在同一个重链上,则一定可以通过某种跳法使得两点并到同一条重链上。为了避免某个点跳完了而另一个点还没开始跳,我们总是希望他们深度差尽可能地小。即:总是让跳完后深度更深的那个点跳。

考虑不严谨的分析复杂度。暴力的复杂度会被这种恶心的特殊形态卡飞,而引入重链后,会发现链上的话重链不会在这里浪费过多的时间,直接跳到分叉上面去。而跳的次数最坏是 \(O(\log n)\) 次。算上维护的数据结构的复杂度(这里默认是线段树)是 \(O(n\log n+q \log^2n)\)

给出部分实现:


class tr_cut{
	public:
		int dep[1000086];//深度
		int fa[1000086];//父亲节点
		int sz[1000086];//子树大小
		int hson[1000086];//重子
		int id[1000086];//新的编号
		int nw[1000086];//新的编号对应的权值
		int top[1000086];//跳过所在重链后到达的位置
		int point;//编号
		void ini(){
			memset(dep,0,sizeof dep);
			memset(fa,-1,sizeof fa);
			memset(sz,0,sizeof sz);
			memset(hson,-1,sizeof hson);
			memset(id,0,sizeof id);
			memset(nw,0,sizeof nw);
			memset(top,0,sizeof top);
			point=0;
		}
		void dfs(int x,int fath){//第一次搜索,确定重子,顺手计算深度
			fa[x]=fath;
		    dep[x]=dep[fa[x]]+1;
		    sz[x]=1;
		    int hs=0;
		    for(int i:e[x])
				if(i!=fath){
			        dfs(i,x);
			        sz[x]+=sz[i];
			        if(sz[i]>sz[hs])
						hson[x]=hs=i;
				}
			
			return;
		}
		void afs(int x,int tp){//第二次,按照重子优先编号,维护top
			id[x]=++point;
		    nw[point]=a[x];
		    top[x]=tp;
		    if(hson[x]==-1)
				return;
		    afs(hson[x],tp);
		    for(int i:e[x])
		    	if(i!=fa[x] and i!=hson[x])
			        afs(i,i);
			return;
		}
}tr;

//封装好的树剖

顺便提一嘴,若对 \(x\) 的子树内进行修改,那么是对新编号中 \(id_x\)\(id_x+sz_x-1\) 进行修改。这样考虑:以 \(x\) 为根的子树内的节点没有遍历完前不会离开该子树,而该子树内共有 \(sz_x\) 个节点,去除掉根节点即 \(id_x\)\(id_x+sz_x-1\)

写树剖要做好大码量,花长时间的准备

posted @ 2024-10-15 20:05  立花廿七  阅读(9)  评论(0编辑  收藏  举报