前言
好像没什么。(湘妹好乖!!!)
进入正题:
动态树
动态树问题
** 若无特殊说明,文中的树指的大多是原树,而非辅助树
定义
维护一个包含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 所在的树
权值信息
- 路径操作:对一条简单路径上的所有对象进行操作
- 树操作:对一棵树上的所有信息进行操作
想要实现的操作
-
C
u
t
(
u
)
Cut( u )
Cut(u)
删除 u u u到它父亲的边,即将以它为根的子树剥离出来。 -
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 是不同的两棵树中的结点 -
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 path−parent )”指针(下文中,将这种指针称作虚边),指向原树上对应优先路径中最高结点的父结点 u u u。但 u u u的儿子指针并不指向 v v v(利用这一性质,可以省略 p a t h − p a r e n t path-parent path−parent 指针,见下文)。
原树与辅助树的关系
- 原树中的重链 -> 辅助树中结点们位于同一棵Splay中。
- 原树中的轻边 -> 辅助树中子结点所在Splay的根节点的path-parent指向父结点。
- 原树与辅助树的结构并不相同。原树是多叉的,而辅助树是二叉排序树。
- 辅助树的根节点 ≠ 原树的根节点。
- 辅助树中的path-parent ≠ 原树中的fa。
- 由于要维护的信息已经都在辅助树中维护了,所以LCT无需维护原树,只维护辅助树即可。
- 原树已经被拆解成一条一条的重链,表达成一组辅助树森林。
- 轻边和重链并不是固定的,随着算法的进行,轻链和重链随算法需要和题目要求而变化,然而无论怎么变化,由这棵辅助树一定能生成原树,并满足辅助树的所有性质。
图解:
一个复杂的例子:
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。在路径树上,这是个分为三步阶段操作:
- 首先,断开 f a fa fa的优先路径上低于 f a fa fa的部分,与处理 x x x的方法相同。即:Splay( f a fa fa ),然后断开它的右子树。
- 接着,连接 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的,但已经变成虚边。
- 最后,做一下辅助操作来完成一次迭代。
我们用相同的办法建立优先路径,直到到达原树的根。
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 操作
查找原树的根结点操作非常容易实现。
- 首先,执行一次Access(
x
x
x ),Splay(
x
x
x ),
x
x
x与原树根将位于同一棵辅助树上,并且
x
x
x是辅助树
的根结点。 - 原树的根结点深度最小,在辅助树上它的键值最小,故位于最左端。因此,从路径树的根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;
}