【学习笔记】动态树(Link-Cut Tree)

137 篇文章 1 订阅
98 篇文章 0 订阅

零、前言

树剖(链分治)有三大类:重链剖分、长链剖分、实链剖分。

其中,前两项都是在树的形态固定的情况下进行操作;而今天所讲的动态树( link-cut tree \text{link-cut tree} link-cut tree)将允许 动态地对树的形态进行更改

对于一条链,用一种数据结构来维护。但是,我们会经常 更改链剖分。因此,一般使用 s p l a y \tt splay splay 使得均摊复杂度为 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 。其余平衡树的复杂度会劣化到 O ( log ⁡ 2 n ) \mathcal O(\log^2 n) O(log2n)

壹、思路与实现

1.定义

对于每一条实链,我们使用一个 s p l a y \tt splay splay键值为深度。那么中序遍历的结果是自顶向下访问的结果。

代码实现上,不需要开很多个 s p l a y \tt splay splay,我们使用这样的操作:认父不认子

每一个小 s p l a y \tt splay splay 的根节点,指向该实链的顶部的父节点。但是父节点不认为它是儿子,即 father ( x ) = y \text{father}(x)=y father(x)=y,但是 x ∉ s o n ( y ) x\notin son(y) x/son(y)

似乎很抽象,拿两张 Y a n g Z h e \rm YangZhe YangZhe 的论文 中的图片:左边是原树,加粗为实边;右边是 s p l a y \tt splay splay 森林,细边是 “认父不认子” 的(也就是说,加粗的边构成了真正的 s p l a y \tt splay splay 森林)。
在这里插入图片描述

本质就是这样的,对于每个小 s p l a y \tt splay splay 来说——

  • 维护的是一条链,键值为深度。
  • father [ r o o t ( x ) ] \text{father}[root(x)] father[root(x)] 是两条实链之间的虚边的上端点。下端点则是 x x x s p l a y \tt splay splay 中深度最小的点。

譬如图右侧的 ⟨ A , G ⟩ \langle A,G\rangle A,G 边,代表的是 t o p ( G ) = C top(G)=C top(G)=C 为下端、 A A A 为上端的虚边,即原树中的 ⟨ C , A ⟩ \langle C,A\rangle C,A

2.核心操作 a c c e s s \rm access access

我们有实链,但是我们没法用。我们得想办法改变实链!

核心的一个操作,就是 access ( x ) \texttt{access}(x) access(x),这会导致 重新划分出一个顶端为根、底端为 x x x 的实链

听上去复杂,代码却异常简单。直接上代码,或许有助于理解。

void access(int x){
	for(int y=0; x!=0; y=x,x=fa[x])
		splay(x), son[x][1] = y, pushUp(x);
}

感受一下这个过程。首先将其旋转至自己的 s p l a y \tt splay splay 的根,这会导致 s p l a y \tt splay splay 变成一个 x x x 为根、左边为其祖先、右边为其子孙的树(请记住, s p l a y \tt splay splay 维护的是一条链)。

y y y 维护的是什么呢?意思是,从调用函数时传入的 x x x,记为 x 0 x_0 x0,到 t o p ( y ) top(y) top(y) 已经接成了一条实链,只是 ⟨ father ( y ) , y ⟩ \big\langle\text{father}(y),y\big\rangle father(y),y 是一条虚边。此时 x = father ( y ) x=\text{father}(y) x=father(y) 。把 x x x 的右端点,也就是其子孙,换成 y y y,相当于把原来的链断开,接上 y y y 所代表的链。

一旦接上,那么 x 0 → y → x x_0\rightarrow y\rightarrow x x0yx 就被打通,所以下一回合中 y = x y=x y=x 。然后迭代地进行处理即可。

形象地感受这一过程。这是作者 缝缝补补 手玩的一组图,不知道怎么做动图。左为原树,右为 s p l a y \tt splay splay 森林。
在这里插入图片描述

  • 第一次, N N N 将其右儿子 O O O 扔掉。
  • 第二次, I I I K K K 当做了其右儿子,然后被 N N N 替换了。
  • 第三次, H H H 失去了 J J J,得到了 I I I
  • 最后一次, A A A B B B 遗弃了,与 H H H 为伍。

仔细地观察这一过程,与我所说的 “丢掉原有的子孙、接上下面的链” 是吻合的(注意看左边的原树)。

3.换根操作

如果我们想要更改树的形态,就需要在树的 “腰” 上加一条边,这在不改变父子关系的情况下不可能做到。所以必须修改原树的根。

怎么让 x x x 成为根呢?众所周知, s p l a y \tt splay splay 支持区间翻转,所以我们要做的就是——

void makeroot(int o){
	access(o), splay(o), flip(o);
}

感受一下这个过程——
在这里插入图片描述
(不好意思画错了,右边的树中 ⟨ A , B ⟩ \big\langle A,B\big\rangle A,B 是虚边才对。)

我把 N N N A A A(原来的根)的实链的深度翻转了一下,相当于我 捏着 N N N 将这条重链提了起来

其他的边肯定是不会变的,因为其他重链的 father \text{father} father 数组是不会变的。很形象啊,不是吗?

4.提取路径

普通的重链剖分,查询任意路径时,需要用 l c a lca lca 拆成两条树链。

但我们的根可以改啊!让路径端点成为根,直接 a c c e s s \tt access access 就提取出来了!

5.原树根

直接 a c c e s s \tt access access 之后找当前 s p l a y \tt splay splay 中深度最小的即可。

int findRoot(int x){
	access(x), splay(x); pushDown(x);
	while(son[x][0] != 0)
		pushDown(x = son[x][0]);
	splay(x); return x;
}

6.判断联通性

实现方法很多。除了下面这种,也可以直接比较 f i n d R o o t \tt findRoot findRoot 的结果。

bool connected(int x,int y){
	makeroot(x);
	return findRoot(y) == x;
}

7.连边

没什么好说的,直接将其中一个搞成根,暴力连接一条虚边即可。

bool link(int x,int y){
	if(connected(x,y)) return false;
	fa[x] = y; return true;
}

注意在 c o n n e c t e d \tt connected connected 中,已经执行了 m a k e r o o t \tt makeroot makeroot splay ( x ) \texttt{splay}(x) splay(x),所以 x x x 已经是树根。另外, c o n n e c t e d \tt connected connected 还包含了 a c c e s s ( y ) {\tt access}(y) access(y),这也是必要的!原因在于 复杂度

8.删边

仍然有多种方法。比如,让某端点成为根之后 a c c e s s \tt access access 它,那么它现在属于大小为 1 1 1 s p l a y \tt splay splay,另一端点必然是其 “虚儿子”,直接将 father \text{father} father 设为 n u l l \rm null null 就结束了。

bool cut(int x,int y){
	if(!connected(x,y)) return false;
	access(x), splay(y), fa[y] = 0;
	return true;
}

同理 connected \texttt{connected} connected 中蕴含了 makeroot ( x ) \texttt{makeroot}(x) makeroot(x) 操作。

9.完整代码

温馨提示:我们在进行 s p l a y \tt splay splay 的时候,由于没有了一路向下查找(顺便下传标记)的过程,我们要手动将路径上的每一个点 pushDown \texttt{pushDown} pushDown 一次。

namespace LCT{
	int fa[MAXN], ch[MAXN][2];
	bool tag[MAXN]; int val[MAXN], w[MAXN];
	inline void flip(const int &o){
		if(o == 0) return ; // ignored
		swap(ch[o][0],ch[o][1]), tag[o] = !tag[o];
	}
	inline void pushDown(int o){
		if(tag[o]) flip(ch[o][0]), flip(ch[o][1]), tag[o] = false;
	}
	inline void pushUp(int o){ // for example
		val[o] = max(w[o],max(val[ch[o][0]],val[ch[o][1]]));
	}
	# define is_rt(o) (ch[fa[o]][0] != (o) && ch[fa[o]][1] != (o))
	# define sonId(o) (ch[fa[o]][1] == (o))
	void rotate(int o){
		int k = fa[o], d = (ch[k][1] == o);
		if((ch[k][d] = ch[o][d^1]) != 0) fa[ch[k][d]] = k;
		if((fa[o] = fa[k]) && !is_rt(k)) ch[fa[k]][sonId(k)] = o;
		ch[o][d^1] = k, fa[k] = o; pushUp(k), pushUp(o);
	}
	void downAll(int o){ // stack simulate recursing
		static int _sta[MAXN], _top; _sta[_top = 1] = o;
		while(!is_rt(o)) _sta[++ _top] = o = fa[o];
		while(_top) pushDown(_sta[_top]), -- _top;
	}
	void splay(int o){
		# define _line(o) (sonId(fa[o]) == sonId(o))
		for(downAll(o); !is_rt(o); rotate(o))
			if(!is_rt(fa[o])) rotate(_line(o) ? fa[o] : o);
	}

	void access(int x){
		for(int y=0; x; y=x,x=fa[x])
			splay(x), ch[x][1] = y, pushUp(x);
	}
	void makeRoot(int x){ access(x), splay(x), flip(x); }
	int findRoot(int x){
		access(x), splay(x);
		while(ch[x][0]) pushDown(x = ch[x][0]);
		splay(x); return x;
	}
	bool is_connected(int x, int y){
		makeRoot(x); return findRoot(y) == x;
	}
	bool link(int x, int y){
		if(is_connected(x,y)) return false;
		access(y), splay(y), fa[x] = y; return true;
	}
	bool cut(int x, int y){
		if(!is_connected(x,y)) return false;
		access(x), splay(y), fa[y] = 0; return true;
	}
}

贰、细节

1.复杂度

这里学来的。显然复杂度都可以放在 s p l a y \tt splay splay a c c e s s \tt access access 上(即,与二者的复杂度同级)。单独调用 splay \texttt{splay} splay 复杂度仍然是 O ( log ⁡ n ) \mathcal O(\log n) O(logn),可见此文或百度搜索。主要讨论 access \texttt{access} access

我们知道 s p l a y \tt splay splay 的势能是 Φ ( T ) = ∑ ϕ ( x ) \Phi(T)=\sum\phi(x) Φ(T)=ϕ(x),其中 ϕ ( x ) = 3 log ⁡ 2 ( ∣ x ∣ ) \phi(x)=3\log_2(|x|) ϕ(x)=3log2(x),其中 ∣ x ∣ |x| x 表示 x x x 的子树大小。结论是,一次 splay ( x ) \texttt{splay}(x) splay(x) 摊还代价不超过 1 + ϕ ( x ′ ) − φ ( x ) 1+\phi(x')-\varphi(x) 1+ϕ(x)φ(x)

我们保留这个势能定义,并且,把虚边也纳入统计。也就是说,虚边和实边共同构成类似 s p l a y \tt splay splay 的东西(尽管它实际上成了多叉的)。仔细对比 splay \texttt{splay} splay 的摊还代价,你会发现其放缩过程只用到了两点: x x x 的势能大于其子节点的势能,与核心不等式 2 log ⁡ 2 ( a + b + 1 ) − log ⁡ 2 a − log ⁡ 2 b ⩾ 1 2\log_2(a+b+1)-\log_2 a-\log_2 b\geqslant 1 2log2(a+b+1)log2alog2b1,因为某处 ∣ x ∣ = ∣ y ∣ + ∣ z ∣ + 1 |x|=|y|+|z|+1 x=y+z+1

在多叉的意义下,只需改为 ∣ x ∣ = ∣ y ∣ + ∣ z ∣ + l |x|=|y|+|z|+l x=y+z+l,其中 l l l 是某个点的虚边所连接的子树大小之和 + 1 +1 +1,所以肯定仍然有 2 ϕ ( x ) − ϕ ( y ) − ϕ ( z ) ⩾ 1 2\phi(x)-\phi(y)-\phi(z)\geqslant 1 2ϕ(x)ϕ(y)ϕ(z)1,仍然能够将常数 1 1 1 给 “塞进” 势能里。

x i x_i xi 为第 i i i splay \texttt{splay} splay 前的 x x x,而 y i y_i yi 则为已 s p l a y \tt splay splay 后的状态。那么每个 splay \texttt{splay} splay 摊还代价不超过 1 + ϕ ( y i ) − ϕ ( x i ) 1+\phi(y_i)-\phi(x_i) 1+ϕ(yi)ϕ(xi) 。又,根据算法流程,可知 x i x_i xi y i − 1 y_{i-1} yi1 的父节点,必然有 ϕ ( x i ) ⩾ ϕ ( y i − 1 ) \phi(x_i)\geqslant\phi(y_{i-1}) ϕ(xi)ϕ(yi1) 。所以总摊还代价不超过
∑ i = 1 k [ 1 + ϕ ( y i ) − ϕ ( y i − 1 ) ] = k + ϕ ( y k ) − ϕ ( x 1 ) \sum_{i=1}^{k}\big[1+\phi(y_i)-\phi(y_{i-1})\big]=k+\phi(y_k)-\phi(x_1) i=1k[1+ϕ(yi)ϕ(yi1)]=k+ϕ(yk)ϕ(x1)

右侧 ϕ ( y k ) − ϕ ( x 1 ) ⩽ 3 log ⁡ 2 n \phi(y_k)-\phi(x_1)\leqslant 3\log _2n ϕ(yk)ϕ(x1)3log2n,所以只需要考虑常数 k k k,也就是经过的虚边数量。它可以用另一个势能分析来说明,由于该势能较简单,甚至可以口述。

定义 重虚边 ⟨ x , y ⟩ \langle x,y\rangle x,y 满足 ∣ x ∣ > 1 2 ∣ y ∣ |x|>\frac{1}{2}|y| x>21y x x x y y y 是虚边,其中 y = father ( x ) y=\text{father}(x) y=father(x) 。注意,它是定义在 s p l a y \tt splay splay 上的,而非原树。势能就是 “重虚边” 的数量。走过 “重虚边” 后,其变为实边,势能 − 1 -1 1,与复杂度相抵。所以摊还代价都在 “轻虚边” 上。而 “轻边” 最多 log ⁡ n \log n logn 条,因为 ∣ y ∣ ⩾ 2 ∣ x ∣ |y|\geqslant 2|x| y2x,子树大小至少翻倍。走过 “轻虚边” 同时还可能让 “重虚边” 增加 1 1 1,所以摊还代价不超过 2 log ⁡ 2 n 2\log_2 n 2log2n

佚闻:似乎是先有的 access \texttt{access} access 复杂度证明,再提出的重链剖分。

注意,这个势能与子树大小有关,所以 link \texttt{link} link cut \texttt{cut} cut 也会改变势能。 cut \texttt{cut} cut 会让 ϕ \phi ϕ 减小,不必担心。那 “重虚边” 数量呢?即 c u t \tt cut cut 导致实边变轻,“重虚边” 浮出水面。可是,这说明原本的 “重实边” 现在是 “轻虚边”,只要是轻的就最多 log ⁡ 2 n \log_2 n log2n 个,倒也没问题。

对于 link \texttt{link} link 操作,则需要谨慎些:若要令 father ( y ) = x \text{father}(y)=x father(y)=x,你应该先 access ( x ) \texttt{access}(x) access(x),这样对势能的影响就局限于 ϕ ( x ) \phi(x) ϕ(x),否则后果不可估量。显然这样对 “重虚边” 的影响也是 Θ ( 1 ) \Theta(1) Θ(1) 级别的。理论上讲,不 access ( x ) \texttt{access}(x) access(x) 会导致 Φ \Phi Φ 异常增大,但我也不会 h a c k \rm hack hack 它……

2.边权

若需要 makeRoot \texttt{makeRoot} makeRoot,在点上维护边权难以 f l i p \tt flip flip 。我能想到的最好解决方案是,将边设置为虚点,连接两个端点。这样 提取路径 的结果仍然无恙。

3.求最近公共祖先

当然 l c a \tt lca lca 的定义需要原树是有根树,假定已经 m a k e R o o t \tt makeRoot makeRoot 了。

a c c e s s ( x ) {\tt access}(x) access(x) 。然后 a c c e s s ( y ) {\tt access}(y) access(y),记录最后一次做 s p l a y \tt splay splay 的点,它就是 l c a lca lca

叁、维护动态 d p \tt dp dp

最近两场比赛都用到了 L C T \tt LCT LCT d d p \tt ddp ddp,但是两场我都惨烈爆零,故忧愁幽思而作此章。

一般地,设 d p \tt dp dp 转移是某个算子 Ω \Omega Ω 。那么 s p l a y \tt splay splay 上维护当前子树的 Ω \Omega Ω 复合结果,关键点在于 a c c e s s \tt access access 时切换虚实边的方法。

新的实儿子,即 y y y,其信息恰好存储在 y y y 上,容易获得。而 x x x s p l a y \tt splay splay 后,其重儿子也就是 x x x s p l a y \tt splay splay 上的右儿子所对应的那条链。所以对于该子树,直接求出 x x x 原来的重儿子的信息即可。

若链底的状态向量 v v v 恰可以写成 Ω v ′ \Omega v' Ωv,那么已知 Ω \Omega Ω 的积时只需代入 v ′ v' v 即可知道当前点的 v v v 。否则,我们需要避免最底层的 Ω \Omega Ω

一种方法是,求值时直接将链底 s p l a y \tt splay splay,这样其左子树就恰好把自己扔掉了,用左子树的 ∏ Ω \prod\Omega Ω 代入 v v v 求值。或者直接维护 “不含深度最小点的 Ω \Omega Ω 积”,这样的话,麻烦全都扔给了 p u s h U p \rm pushUp pushUp

一般而言,代码形如

void access(int x){
	for(int y=0; x; y=x,x=fa[x]){
		splay(x); // before got
		if(ch[x][1] != 0) got(x,ch[x][1]);
		if(y != 0) lost(x,y), splay(y);
		ch[x][1] = y, pushUp(x);
	}
}

l o s t \tt lost lost 之后调用的 s p l a y \tt splay splay 并非必要(取决于具体代码实现)。

最后,为了解决 Ω \Omega Ω 构成非交换群的情况,需要维护一个 “自底向上” 的乘积和 “自顶向下” 的乘积,才能完成 f l i p \tt flip flip 。当然在部分无需 m a k e R o o t \tt makeRoot makeRoot 的题目中就不用管。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值