树链剖分作业小结(上)

前言

这几天疯狂地在补树链剖分的坑,也刷了几道题,这里做个小结。

树链剖分

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。(摘自Oi wiki)。

常见的树链剖分主要是重链剖分。
即定义某一节点的的子节点中子树大小最大的一点为它的重儿子,在dfs遍历时优先遍历。连接这两个点的边成为重边,若干重边相连构成了一条重链。

值得注意的是,因为重儿子和重边是对每一节点都有定义的,所以整棵树是被分成了若干条重链的,每一个节点也都属于且仅属于一条重链。
如下图。(依旧摘自Oi wiki)依旧摘自Oi wiki
在上图中,我们可以发现一条重链上的dfs序是连续的,这就启示我们可以用一些维护序列的数据结构来维护这棵树。那用什么呢.(小声bb:肯定不是线段树)
线段树是维护序列信息的老大哥了,所以它来了。(于是你的树剖代码动辄100+ 200+,麻了)
我们可以十分便利地利用线段树来维护许多树上的信息,比如说路径上的最大值,以及累加和。

实现

实现树链剖分一般需要7个数组。

s z e [ x ] sze[x] sze[x]:以 x x x为根的子树大小。
d e p [ x ] dep[x] dep[x] x x x节点的深度。
s o n [ x ] son[x] son[x] x x x的重儿子。
f a [ x ] fa[x] fa[x] x x x的父节点。
t o p [ x ] top[x] top[x] x x x所处重链的链顶。
d f n [ x ] dfn[x] dfn[x] x x x d f s dfs dfs序。
r n k [ x ] rnk[x] rnk[x]: d f s dfs dfs序为 x x x的点的编号。有 r n k [ d f n [ x ] ] = x rnk[dfn[x]]=x rnk[dfn[x]]=x 比较绕。
而在处理有关子树的问题时,我们通常需要额外记录一个 e n en en数组表示以 x x x为根的子树的结束,利用子树内的 d f s dfs dfs序同样连续来维护信息。
下面给出本蒟蒻常用的模板。(前向星存图)

struct edge{
   
	int y,nxt;
}Edge[N<<1];//前向星存图 
int Link[N],sze[N],fa[N],dep[N],son[N],len=0,dfn[N],rnk[N],top[N],tot=0,n,m,v[N];
void insert(int x,int y) {
   
	Edge[++len]={
   y,Link[x]};
	Link[x]=len;
}//加边函数 
void dfs1(int now,int fath) {
   //求出sze,fa,dep,son 
	dep[now]=dep[fath]+1;sze[now]=1;fa[now]=fath;son[now]=-1;//对当前节点的处理 
	for(int i=Link[now];i;i=Edge[i].nxt) {
   
		int y=Edge[i].y;
		if(dep[y]) continue;
		dfs1(y,now);
		sze[now]+=sze[y];//加上它儿子的子树大小 
		if(son[now]==-1||sze[y]>sze[son[now]]) son[now]=y;//更新重儿子 
	}
}
void dfs2(int now,int ttp) {
   //求出top,dfn,rnk。ttp表示链顶 
	top[now]=ttp;dfn[now]=++tot;rnk[tot]=now;
	if(son[now]==-1) return ;//没有重儿子,说明是叶节点。return 
	dfs2(son[now],ttp);//优先遍历重儿子 
	for(int i=Link[now];i;i=Edge[i].nxt) {
   
		int y=Edge[i].y;
		if(y==fa[now]||y==son[now]) continue;
		dfs2(y,y);//对于轻儿子来说,它是新一条重链的链顶 
	}
}

时间复杂度

可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 l c a lca lca 分别向两边往下走,分别最多走 l o g n logn logn 次,因此,树上的每条路径都可以被拆分成不超过 l o g n logn logn 条重链。
(还是Oi wiki)
所以其时间复杂度是 n l o g 2 n nlog^2n nlog2n的~~(本蒟蒻不会,记住就好)~~

例题

lg P3379 【模板】最近公共祖先(LCA)
题面描述

给定n个点的树(1是根),m次询问,每次询问两点的LCA;

做法

一个做法多样的板子题。可以倍增,当然也可以树剖求。只需要对于查询的两个节点,在其 t o p top top不相等的时候,每次让节点深度大的那个向上跳即可。
当二者处于同一重链中时,深度较小的那个即为 l c a lca lca
核心代码如下

int lca(int x,int y) {
   
	while(top[x]!=top[y]) {
   
		if(dep[top[x]]>dep[top[y]] ) x=fa[top[x]];
		else y=fa[top[y]];
	}
	return dep[x]>dep[y]?y:x;
}

请注意这个 w h i l e while while循环,几乎所有路径上操作都需要用到它,务必按上面的图手推一遍,确保深刻理解。

HDOJ2586 how far away?(lg不知道哪道题)
题面描述

给出 个点的一棵树,多次询问两点之间的最短距离。

做法

本质和上一道题并无区别,只需在 d f s 1 dfs1 dfs1中计算出每个节点到根节点的距离 d i s dis dis,最后答案即为 d i s [ x ] + d i s [ y ] − 2 ∗ d i s [ l c

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值