Link-Cut Tree(动态树)

前言

好像没什么。(湘妹好乖!!!)
进入正题:

动态树

动态树问题

** 若无特殊说明,文中的树指的大多是原树,而非辅助树

定义

维护一个包含N个点的森林,并且支持形态和权值信息的操作。

形态信息
  • L i n k ( u , v ) Link(u, v) Link(u,v) // 添加边 ( u , v ) (u, v) (u,v)
  • C u t ( u , v ) Cut(u, v) Cut(u,v) // 删除边 ( u , v ) (u, v) (u,v)
  • F i n d ( u ) Find( u ) Find(u) // 找到点 u u u 所在的树
权值信息
  • 路径操作:对一条简单路径上的所有对象进行操作
  • 树操作:对一棵树上的所有信息进行操作

想要实现的操作

  1. C u t ( u ) Cut( u ) Cut(u)
    删除 u u u到它父亲的边,即将以它为根的子树剥离出来。
  2. L i n k ( u , v ) Link(u,v) Link(u,v)
    增加边 ( u , v ) (u,v) (u,v),使 v v v成为 w w w的儿子 ( p a r e n t ( u ) ← v ) (parent(u) ← v ) (parent(u)v)。须确保 u u u是根
    结点,且 u u u v v v 是不同的两棵树中的结点
  3. E v e r t ( u ) Evert(u) Evert(u)
    通过把 u u u变成所在树的根,使这棵树 u u u “外翻(Inside Out)” 出来

一些概念

原树

  • 是若干有根树组成的森林,每棵树与标准的有根树无异,只不过增加了一个“优先”的概念。

优先

  • 一个结点可能“偏好”某个儿子,使它成为“优先儿子”。类似于树链剖分中的“重儿子”。
  • 优先儿子与它父亲之间的边称为“优先边”。类似于“重边”
  • 仅由优先边组成的路径称为“优先路径”。类似于“重链”优先路径。有可能只有一个结点。
  • 需要注意的是,用树链剖分的对应概念来类比,只是帮助大家尽快理解动态树中引入的新概念。两者的区别是显著的:重儿子、重边、重链等是静态的,而优先关系是动态的,随时可能变化。

通常,我们遇到的问题都是在原树上的。因此,我们首先搞清楚在原树上要执行的是什么特定操作,然后解释这个操作在Link-Cut Tree上怎样执行。

Link-Cut Tree

路径树

路径树是用来表示原树上的优先路径的树。路径树用Splay实现,结点是原树的一条优先路径上所有结点,并以结点在原树上的深度为关键字。路径树也叫辅助树。(与树链剖分中的辅助树类似,但仍是动态和静态的区别)

核心思想

Link-Cut tree的核心思想是把原树剖分成若干优先路径,然后把每条优先路径用一棵对应的路径树表示。准确地说,原树剖分后是一组辅助树森林

每棵路径树的根结点 v v v都有一个“路径-父亲( p a t h − p a r e n t path-parent pathparent )”指针(下文中,将这种指针称作虚边),指向原树上对应优先路径中最高结点的父结点 u u u。但 u u u的儿子指针并不指向 v v v(利用这一性质,可以省略 p a t h − p a r e n t path-parent pathparent 指针,见下文)。

原树与辅助树的关系
  • 原树中的重链 -> 辅助树中结点们位于同一棵Splay中。
  • 原树中的轻边 -> 辅助树中子结点所在Splay的根节点的path-parent指向父结点。
  • 原树与辅助树的结构并不相同。原树是多叉的,而辅助树是二叉排序树。
  • 辅助树的根节点 ≠ 原树的根节点。
  • 辅助树中的path-parent ≠ 原树中的fa。
  • 由于要维护的信息已经都在辅助树中维护了,所以LCT无需维护原树,只维护辅助树即可。
  • 原树已经被拆解成一条一条的重链,表达成一组辅助树森林。
  • 轻边和重链并不是固定的,随着算法的进行,轻链和重链随算法需要和题目要求而变化,然而无论怎么变化,由这棵辅助树一定能生成原树,并满足辅助树的所有性质。
图解:

箭头的有向边表示path-parent指针
一个复杂的例子:
在这里插入图片描述

Access 操作

Access 操作是 Link-Cut Tree 的所有操作的基础。调用 Access( v ),那么在原树上从结点 v v v 到根结点的路径就成为一条新的优先路径,看上去就像 v v v刚被访问过一样。并且在Link-Cut Tree对应的路径树上, v v v成为根结点。

如果原树上v到根路径上经过的某条边 ( u , f a t h e r ) (u, father) (u,father)原不是优先边,就会变成优先边,那么原本包含 f a t h e r father father的优先路径将从 f a t h e r father father处断开。也就是说,当新的优先路径形成后,原来的一些优先路径会发生
改变:那些有一个端点在新路径上的优先边被取消,改成虚边。

在这里插入图片描述
Link-Cut Tree上结点以是它们在原树上的深度为关键字的。 x x x左子树的结点都高于 x x x,右子树的结点都低于 x x x。由于访问 x x x后,它的优先儿子要取消,如果访问前, v v v在一条优先路径的中段,那么访问后低于x的部分要分离出去。在路径树上如何操作呢?
最简单的办法是对 x x x作一次伸展(Splay),把结点 v v v提升为根。它是新的优先路径中深度最大的结点,所以它应该没有右儿子,故需要断开 v v v与右子树。
从理论上讲,需要把右子树根结点的虚边指向 x x x,断开 x x x与右子树,使之成为另一棵单独的路径树。代码上只需要 x x x指向NIL,就断开了与原儿子的链接。

处理完低于 v v v的部分,接下来看如何从 v v v向上建立到根的优先路径:

Splay( x x x )后, x x x 成为根,它的 f a fa fa指向另一棵辅助树的某个结点(称作 f a fa fa)。我们要设置 f a fa fa的优先儿子是 x x x。在路径树上,这是个分为三步阶段操作:

  1. 首先,断开 f a fa fa的优先路径上低于 f a fa fa的部分,与处理 x x x的方法相同。即:Splay( f a fa fa ),然后断开它的右子树。
  2. 接着,连接 x x x的路径树与 f a fa fa的路径树。因为 x x x树上所有结点都低于 f a fa fa树上所有结点,所以我们只需要把 x x x设为 f a fa fa的右儿子。注意, f a fa fa原来的右儿子仍然指向 f a fa fa的,但已经变成虚边。
  3. 最后,做一下辅助操作来完成一次迭代。

我们用相同的办法建立优先路径,直到到达原树的根。
Code:

inline void Access(LL x)
	{
		for (Int y = 0; x; x = Tree[y = x].Fa)
		{
			Splay( x );
			Tree[x].Son[1] = y;
			PushUp( x );
		}
	}

FindRoot 操作

查找原树的根结点操作非常容易实现。

  1. 首先,执行一次Access( x x x ),Splay( x x x ), x x x与原树根将位于同一棵辅助树上,并且 x x x是辅助树
    的根结点。
  2. 原树的根结点深度最小,在辅助树上它的键值最小,故位于最左端。因此,从路径树的根x
    开始一直往左儿子方向走,当走不动时,我们就找到了原树根。
    Code:
inline LL FindRoot(LL x)
	{
		Access( x );
		Splay( x );
		while (Tree[x].Son[0])
			PushDown( x ), x = Tree[x].Son[0];
		Splay( x );
		return x;
	}

MakeRoot 操作

MakeRoot是将 x x x提领为原树的根。

对于普通的无根树可以任意将某个结点提领成根,有向树没有这个操作。

注意Access( x x x )之后 x x x只是辅助树的总根,并不是原树的根,因为Access( x x x )之后 x x x还有左子树,左子树的深度小于 x x x,故 x x x不是根节点。发现 x x x设为根之后,原先 x x x到根的路径上点的深度正好反转。于是只需要在Access + Splay之后翻转即可。
Code:

void MakeRoot(LL x)
	{
		Access( x );
		Splay( x );
		PushDown_Reverse( x );
	}

Link 操作

将包含结点 x x x和包含 y y y的两棵子树连接越来,成为一棵树。
无向图先把 x x x提领成所在子树的根,再连接到 y y y(相当于用虚边指向 y y y)。
Code:

inline void Link(LL x,LL y)
	{
		MakeRoot( x );
		if (FindRoot( y ) != x)
			Tree[x].Fa = y;
	}

Cut 操作

Cut( x , y x, y x,y)操作将 x x x y y y之间的连边切断。方法是:直接将 x x x提领成根,然后将 y y y进行Access + Splay,之后 x x x一定是 y y y的左儿子,直接切断即可。
Code:

inline void Cut(LL x,LL y)
	{
		MakeRoot( x );
		if (FindRoot( y ) == x && Tree[y].Fa == x && ! Tree[y].Son[0])
		{
			Tree[y].Fa = Tree[x].Son[1] = 0;
			PushUp( x );
		}
	}

关于x-y路径上的查询与修改

x x x y y y路径上的点进行修改或查询,只需要对 x x x进行MakeRoot操作,然后对 y y y进行Access + Splay操作,那么 x x x y y y路径上的所有点都在以y为根的子树上,之后就方便处理了。

实现细节:Link-Cut Tree的初态

一个令初学者困惑的问题是:Link-Cut Tree是怎样生成的?其实Link-Cut Tree是不需要额外做Build生成的。它的初态就是每个结点都是一棵路径树!
在DFS生成原树形态后,每个结点的 f a t h e r father father指针都指向它的父结点,而此时每个结点的左、右儿子指针都等于NIL,表明它们每一个都是单独一棵路径树。此后,在执行Access操作时,会生成一些更长的路径树。(即一开始,将每个点都看作一个Splay)

另一个令初学者困惑的问题是:Link-Cut Tree是许多棵路径树,每棵路径树都是由Splay Tree来表示,那么需要维护许许多多的Splay Tree吗?
答案是:不需要!只要一棵Splay Tree就够了。因为每棵路径树就是整棵树的一部分,只要做Splay操作时保证不伸展到这棵路径树外边去就行了。这是由IsRoot()函数来做判断的。换言之,在一棵Splay上,有父-子双向链接的属于同一棵路径树上的结点。只有子->父单向链接的是不同路径树上的结点。

练习

洛谷板题
Code:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAXN 100005
#define LL long long
#define Int register int
using namespace std;
inline void read(LL &x)
{
	x = 0;
	LL f = 1;
	char s = getchar();
	while (s < '0' || s > '9')
	{
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s <= '9')
	{
		x = (x << 3) + (x << 1) + (s ^ 48);
		s = getchar();
	}
	x *= f;
}
inline LL Max(LL x,LL y)
{
	return x > y ? x : y;
}
inline LL Min(LL x,LL y)
{
	return x < y ? x : y;
}
inline void Swap(LL &x,LL &y)
{
	LL temp = x;
	x = y;
	y = temp;
}
struct DTTree
{
	struct SplayTree
	{
		bool Lazy_Reverse;
		LL Fa, Son[2], Size, Sum, Val;
	}Tree[MAXN << 1];
	inline bool NotRoot(LL x)
	{
		if (Tree[Tree[x].Fa].Son[0] == x || Tree[Tree[x].Fa].Son[1] == x)
			return 1;
		return 0;
	}
	inline void PushDown_Reverse(LL x)
	{
		Swap(Tree[x].Son[0], Tree[x].Son[1]);
		Tree[x].Lazy_Reverse ^= 1;
	}
	inline void PushUp(LL x)
	{
		Tree[x].Sum = Tree[Tree[x].Son[0]].Sum ^ Tree[Tree[x].Son[1]].Sum ^ Tree[x].Val;
	}
	inline void PushDown(LL x)
	{
		if (Tree[x].Lazy_Reverse)
		{
			if (Tree[x].Son[0])
				PushDown_Reverse(Tree[x].Son[0]);
			if (Tree[x].Son[1])
				PushDown_Reverse(Tree[x].Son[1]);
			Tree[x].Lazy_Reverse = 0;
		}
	}
	inline void Rotate(LL x)
	{
		LL y = Tree[x].Fa;
		LL z = Tree[y].Fa;
		bool Fx = Tree[y].Son[1] == x;
		LL B = Tree[x].Son[Fx ^ 1];
		if (NotRoot( y ))
			Tree[z].Son[Tree[z].Son[1] == y] = x;
		Tree[x].Son[Fx ^ 1] = y;
		Tree[y].Son[Fx] = B;
		if ( B )
			 Tree[B].Fa = y;
		Tree[y].Fa = x;
		Tree[x].Fa = z;
		PushUp( y );
	}
	inline void Splay(LL x)
	{
		static LL St[MAXN];
		LL y = x, cnt = 0;
		St[++ cnt] = y;
		while (NotRoot( y ))
			St[++ cnt] = y = Tree[y].Fa;
		while ( cnt )
			PushDown(St[cnt --]);
		while (NotRoot( x ))
		{
			y = Tree[x].Fa;
			LL z = Tree[y].Fa;
			if (NotRoot( y ))
				Rotate((Tree[y].Son[0] == x) ^ (Tree[z].Son[0] == y) ? x : y);
			Rotate( x );
		}
		PushUp( x );
	}
	inline void Access(LL x)
	{
		for (Int y = 0; x; x = Tree[y = x].Fa)
		{
			Splay( x );
			Tree[x].Son[1] = y;
			PushUp( x );
		}
	}
	void MakeRoot(LL x)
	{
		Access( x );
		Splay( x );
		PushDown_Reverse( x );
	}
	inline LL FindRoot(LL x)
	{
		Access( x );
		Splay( x );
		while (Tree[x].Son[0])
			PushDown( x ), x = Tree[x].Son[0];
		Splay( x );
		return x;
	}
	inline void Split(LL x,LL y)
	{
		MakeRoot( x );
		Access( y );
		Splay( y );
	}
	inline void Link(LL x,LL y)
	{
		MakeRoot( x );
		if (FindRoot( y ) != x)
			Tree[x].Fa = y;
	}
	inline void Cut(LL x,LL y)
	{
		MakeRoot( x );
		if (FindRoot( y ) == x && Tree[y].Fa == x && ! Tree[y].Son[0])
		{
			Tree[y].Fa = Tree[x].Son[1] = 0;
			PushUp( x );
		}
	}
}Fuck_;
int main()
{
	LL n, m;
	read( n ); read( m );
	for (Int i = 1; i <= n; ++ i)
		read( Fuck_.Tree[i].Val );
	for (Int i = 1; i <= m; ++ i)
	{
		LL Or, x, y;
		read( Or ); read( x ); read( y );
		switch ( Or )
		{
			case 0:
			{
				Fuck_.Split(x, y);
				printf("%lld\n", Fuck_.Tree[y].Sum);
				break;
			}
			case 1:
			{
				Fuck_.Link(x, y);
				break;
			}
			case 2:
			{
				Fuck_.Cut(x, y);
				break;
			}
			case 3:
			{
				Fuck_.Splay( x );
				Fuck_.Tree[x].Val = y; 
			}
		}
	}
	return 0;
}

[COCI 2009] OTOCI / 极地旅行社

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值