树链剖分(萌新用,复习用)

树链剖分是将一棵树剖分成一条一条链,从而将树上问题转换为序列问题的操作,最常见的是轻重链剖分,这里也注重讲轻重链剖分。

第一步:找重儿子。

对于这棵树,我们想让这棵树被剖分成一棵序列,那么就要先找到每个节点的重儿子。重儿子的定义是以儿子为根的子树大小是所有儿子中最大的。

如本图,\(4\)\(2\) 的儿子,子树大小为 \(1\)\(5\)\(2\) 的儿子,子树大小为 \(3\)。所以 \(5\)\(2\) 的重儿子。

用搜索遍历树,再回溯上来,进行比较即可。

int dfs1(int x,int fath)
{
	siz[x]=1;
	int len=a[x].size(),maxn=0;
	for(int i=0;i<len;i++)
	{
		if(a[x][i]==fath)continue;
		int num=dfs1(a[x][i],x);
		if(num>maxn)maxn=num,son[x]=a[x][i];
		siz[x]+=num;
	}
	return siz[x];
}

这样子的话,父亲节点与重儿子所连接的边为重边,图中 \((1,2),(2,5),(5,6),(6,7)\) 皆为重边。

重边收尾连接即为重链,图中 \((1,7)\) 即为重链。当然树上重链可以不止一条。而图中 \((2,4),(1,3)\) 为轻链。

注:首位相连的轻边不是轻链,单独的轻边才叫轻链。

第二步:找到自己所处的重链的顶端。

如图中 \(6\) 处于重链 \((1,7)\) 上,所以 \(6\) 所处链的顶端为 \(1\)

又如 \(2\) 在重链 \((1,7)\) 与轻链 \((2,4)\) 上,那么这种情况把 \(2\) 优先看做在重链上,所以顶端为 \(1\)

若一个节点在轻链上,那么它的顶端是它自己,如 \(4\) 的顶端是 \(4\)

代码实现:搜索遍历,先搜重儿子,然后重儿子的顶端为父亲节点所处的顶端,将参数往下带,再搜轻儿子,改变顶端为它自己,搜下去即可。

void dfs2(int x,int tp)
{
	topp[x]=tp;
	if(son[x])dfs2(son[x],tp);
	int len=a[x].size();
	for(int i=0;i<len;i++)
	{
		if(a[x][i]==fa[x]||a[x][i]==son[x])continue;
		dfs2(a[x][i],a[x][i]);
	}
}

到此轻重链剖分的所有已经结束了,现在将轻重链剖分的应用。

1、LCA

可以证明一个点到根节点的重链个数小于等于 \(\log(n)\)

证明:因为每分开一个重链,那么就必须有另一棵子树的大小大于等于这棵树,模拟下发现是棵完全树,所以最小为 \(\log(n)\)

所以每一次对于两个节点,将自己对应顶部的节点所处深度大的往上跳,跳到自己顶部的父亲节点。直到两个节点所处一个重链,即顶部相同,那么此时深度小的那个点即是两点的 LCA。

int LCA(int x,int y)
{
	while(topp[x]!=topp[y])
	{
		if(dep[topp[x]]<dep[topp[y]])swap(x,y);
		x=fa[topp[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	return x;
}

2、数据结构。

常见为线段树。

在找重链时加上时间戳,代表在线段树中的节点编号。

void dfs2(int x,int tp)
{
	topp[x]=tp;
	id[x]=++cnt;
	if(son[x])dfs2(son[x],tp);
	int len=a[x].size();
	for(int i=0;i<len;i++)
	{
		if(a[x][i]==fa[x]||a[x][i]==son[x])continue;
		dfs2(a[x][i],a[x][i]);
	}
}

操作1:对 \(x\) 的子树操作,即为对编号 \(x\) 到 编号 \(x+siz_x-1\) 进行区间操作。\(siz_x\) 代表以 \(x\) 为根的子树大小。

操作2:对 \(x\)\(y\) 的路径操作,找到 \(x\)\(y\) 的 LCA,在搜 LCA 时对每一条重链进行 \(top_x\)\(x\) 的区间操作,最后再对 \(x\)\(y\) 的区间操作。

注:以上操作都是树上节点对应的线段树编号进行操作。

void update_tree(int x)//对整棵子树加上一个值
{
	nl=id[x],nr=id[x]+siz[x]-1;
	update(1,1,n);
}
int search_road(int x,int y)//查询一条路径上的值
{
	int sum=0;
	while(topp[x]!=topp[y])
	{
		if(dep[topp[x]]<dep[topp[y]])swap(x,y);
		nl=id[topp[x]],nr=id[x];
		sum+=search(1,1,n);
		x=fa[topp[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	nl=id[x],nr=id[y];
	sum+=search(1,1,n);
	return sum;
}

基本上所有树剖的基础操作就到这了,总体来说思路挺简单的,就是代码有亿点复杂。

练习题:

P3384 【模板】轻重链剖分/树链剖分(模板题)

P3313 [SDOI2014]旅行(较简单的变式题)

P2590 [ZJOI2008]树的统计(练习题)

P1505 [国家集训队]旅游(练习题)

P2486 [SDOI2011]染色(较简单的变式题)

P3258 松鼠的新家(练习题)

P4069 [SDOI2016]游戏(合李超线段树)

P4211 [LNOI2014]LCA(树剖求区间 LCA,较难想到)

P4592 [TJOI2018]异或(树剖+可持久化01trie)

P5305 [GXOI/GZOI2019]旧词(会了 P4211 不算特别难)

P5354 [Ynoi2017] 由乃的 OJ(Ynoi的题,你觉得呢)

P5499 [LnOI2019]Abbi并不想研学(双 laz 树剖题,树剖套模板线段树 2)

SP6779 GSS7 - Can you answer these queries VII(这些题中码量最大的,求树上子段和最大值,树剖套小白逛公园)

posted @ 2023-02-24 14:02  Gmt丶Fu9ture  阅读(82)  评论(0)    收藏  举报