您的位置:首页 > 其它

Link-Cut Tree(Insider Preview)

2018-04-02 19:42 204 查看
以下内容仅供指教,请勿当真。

如有侵权,请与我联系(我相信是没有的)。

Link-Cut Tree
总览

1. 动态树问题

2. 树链剖分

3. LCT

4. Splay

入门

1. LCT 的形态

2. 概念与操作
①偏爱子结点/边/路径

②访问操作:access

③置根操作:makeroot

④link 操作

⑤cut 操作

3. e.g. Luogu 2147 [SDOI 2008] 洞穴勘测
findroot 操作

4. 实现5
①Splay 结点

②parent 指针

③rotate 操作和 Splay 操作

④access 操作

⑤makeroot 和 findroot 操作

⑥link 和 cut 操作

⑦初始化

5. 时间复杂度

进阶

e.g. Luogu 3203 弹飞绵羊
①维护 size

②获取结点在原树中的深度

e.g. Luogu 3690 [模板] Link-Cut Tree
①解决 cut 的合法性问题

②维护异或和

e.g. Luogu 2387 [NOI 2014] 魔法森林
①边权转点权

②维护最小生成树

开局一个 Markdown,内容全靠编。——Orange

Link-Cut Tree

参考资料

参考资料

  不保证能看懂,但保证讲清楚所有细节。

总览

1. 动态树问题

  动态树问题是一类要求维护森林的连通性的题的总称。这类问题要求:

维护某个点到根的某些数据。

对树进行切分。

对树进行合并。

操作子树。

……

  解决这一问题的的子问题(不包括操作子树)的基础数据结构就是 Link-Cut Tree(LCT)。

2. 树链剖分

  静态的树链剖分将树分成了若干条链1。最简单有效的剖分方式为重链剖分:对于某个结点,将连向最大子树的边称为重边,其余边称为轻边,每一条链都由重边组成(称为重链),而每两条链之间由轻边连接。这样可以保证任意一个结点到根结点的路径上经过的轻边数量为 O(logn)O(log⁡n),因此我们可以用数据结构维护每条重链,每次至多修改 O(logn)O(log⁡n) 次数据结构。

3. LCT

  LCT 的核心思想也是将树分成若干条链,对于每一条链,我们都用 Splay 来进行维护。由于 LCT 解决的是动态树问题,所以链并不是一成不变的,而是需要不断地进行拼接,因此 Splay 是我们的不二之选。

4. Splay

  复习一下,为什么使用 Splay:虽然说 Splay 是一棵二叉搜索树,但是它不一定非要用作一棵二叉搜索树,而是可以看作一个支持均摊 O(logn)O(log⁡n) 进行分裂和合并的数组。

入门

1. LCT 的形态

  我们有一棵树:

![](pic\LCT 1.png)

  看上去它是一棵以 11 为根的树,但是没有关系,在 LCT 中,我们不管原树有根还是无根,我们只需要知道原树的根的编号就可以了,换句话说,原树的根是哪个和 LCT 要做的事情无关。

  那我们怎么维护原树呢?刚刚说了,我们将原树分成了一条条链,我们只需要维护每条链和链之间的关系就好了。对于每一条重链,我们用一棵 Splay 来维护;将 Splay 看作一个数组,我们保存的结点在原树中的深度和在 Splay 中的下标的增减性是一致的:

![](pic\LCT 2.png)

拆分成的三条链

![](pic\LCT 3.png)

三条链对应的 Splay 可能的形态

  例如,上图中,结点 11,22,33 构成一棵 Splay,中序遍历后的深度是递增的。注意,Splay 的形态可能和链在原树中的形态不一样(可能不再是链)。

  那么 LCT 的链之间用什么维护呢?还记得树链剖分中的 toptop 吗?我们称一条链中深度最小的点为这条链的 toptop。在 LCT 中,对于每棵 Splay,我们从 Splay 的根结点(没错,Splay 的根结点)向与原树中这棵 Splay 对应重链的 toptop 相邻2的结点连一条有向边(这个结点不在这条链上)

![](pic\LCT 4.png)

LCT 的实际形态。上图中,双向边代表 Splay 的边,单向边代表连接 Splay 的边。

  不难发现,如果将每一个 Splay 缩成一个点,那么各点之间的关系仍然是树形关系。我们称这棵 Splay 组成的树的根结点(注意它是棵 Splay)的中序遍历的第一个点为对应连通块在 LCT 中的根结点 rootroot。再次强调,这个“LCT 中的根结点”和原树中的根结点没有一点关系,哪怕原树是棵无根树,还是有 LCT 中的根结点一说。当然,如果我们维护的是一个森林,那么森林中的每一个连通块在 LCT 中都会存在一个对应的根结点。

2. 概念与操作

①偏爱子结点/边/路径

  根据上面的描述,LCT 实际上也是对原树进行了树链剖分。对于一个结点,我们称原树中与它在同一条链上的它的儿子(如果存在)为偏爱子结点(preferred child);称连接一个结点和它的偏爱子结点的边为偏爱边(preferred edge);称一条链为偏爱路径(preferred path)。

  以上概念和轻重链剖分中的重儿子、重边和重链完全类似,且在 LCT 的形态中说得很清楚了,所以我们立马可以得到以下结论:

每个点在且仅在一条偏爱路径上。

所有的偏爱路径包含了这棵树上所有的点。

(上面不是废话吗,LCT 的形态中已经说得很清楚了)

②访问操作:access

  访问(access)操作是 LCT 的核心操作,也是体现 LCT “动态”的操作。

  假设现在的 LCT 中只有一个连通块,且根是 rootroot,我们定义 access(x)access(x) 为:令 xx 到 rootroot 的路径成为一条偏爱路径,且令 xx 的子结点和 rootroot 的其它子结点都不在这条偏爱路径上。

![](pic\LCT 2.png)

假设的一开始的偏爱路径(原树为前面的那棵树,忽略了非偏爱边)

![](pic\LCT 5.png)

access(6)access(6) 后的偏爱路径(忽略了非偏爱边)

  然而实际上,我们是如何实现 access 操作的呢?

  假设我们正在进行 access(x)access(x),我们通过旋转 xx 让 xx 成为 Splay 的根结点,这样,xx 的右子树便是它的重儿子了,将它断开,然后让它向 xx 连一条非偏爱边。

  根据之前 LCT 的形态,我们是可以知道 xx 在原树中的父结点的(因为现在它是它所在的 Splay 的根,保存了它在原树中相邻的结点)。它的父结点对应的偏爱路径无非就样:

![](pic\LCT 6.png)

除了 xx 到 33 的边不是偏爱边,其它边都是偏爱边

  我们找到 33 结点,对它进行 Splay 操作3,那么 33 的右子树就是 33 之前的偏爱儿子了(44 结点)。我们断开它,从 44 向 33 连一条非偏爱边,然后把 xx 接到 33 的右子树上,就得到了新的 Splay。不断重复这个过程,直到对 xx 进行 Splay 操作后发现不存在一条从 xx 出发的非偏爱边。

  需要注意的是,在 access 途中要随时维护信息,不过现在还没有说到代码,所以就暂时不用管啦。另外,access(x)access(x) 之后 xx 并不是对应 Splay 的根,如果需要,可以对 xx 进行 Splay 操作。

③置根操作:makeroot

  前面的注释和 access 的介绍中也提到过,LCT 中的每一个连通块都有一个根结点,且需要支持换根操作。假设目前 LCT 的根为 rootroot(再次强调是 LCT 的根,与原树的根无关,原树甚至没有根),我们想让 xx 成为新的根,那么我们首先 access(x)access(x)。

  在此操作后,我们发现只需要将 xx 所在的偏爱路径进行翻转就可以了。因为这样做之后, xx 所在偏爱路径各个结点的深度正好调换了顺序,而其它偏爱路径各结点的相对深度并不发生改变,因此我们利用 Splay 的翻转操作,将 xx 所在的整个偏爱路径翻转即可。

![](pic\LCT 7.png)

翻转 xx 所在偏爱路径后,其它偏爱路径的相对深度不改变

  为了方便,我们直接对 xx 进行 Splay 操作,则 xx 成为了当前 Splay 的根,我们对它打上翻转标记4即可。

④link 操作

  顾名思义,link 操作将两个不在同一连通块的点连接起来。

  假设这两个点为 xx 和 yy,我们首先调用 makeroot(x)makeroot(x),然后我们实际上令 xx 成为 yy 的子结点就好了。我们从 xx 向 yy 连一条非偏爱边即可。

⑤cut 操作

  顾名思义,cut 操作将两个相邻结点断开。

  假设这两个点为 xx 和 yy,我们首先调用 makeroot(x)makeroot(x),然后我们再调用 access(y)access(y),然后实际上我们只需要把 xx 到 yy 的偏爱边断开就好了。我们对 yy 进行 Splay 操作,则 xx 一定为 yy 的左儿子。将这条边断开,维护 yy 的信息即可。

3. e.g. Luogu 2147 [SDOI 2008] 洞穴勘测

  我们通过一道例题来引入具体实现和基本应用。

题目大意:有一个结点数为 nn 的森林,有 mm 次操作,操作分为三种:连接两个结点,删去两条边,查询两个点的连通性。保证操作合法,任意一次操作后森林还是森林。

  这道题要我们做的是动态维护森林的连通性,支持加边删边,这正是 LCT 擅长的。

findroot 操作

  findroot 操作允许我们找到某个结点所在连通块在 LCT 中的根结点。思路也很简单:直接 access(x)access(x),然后对 xx 进行 Splay 操作,然后沿着 Splay 结点的左儿子走,找到 Splay 中序遍历的第一个结点就好了。

  那么这道题的做法就是:对于 Query 操作,我们调用 findroot,检查两点在 LCT 中的根结点是否一样(一样就说明在同一连通块,否则就说明在不同连通块);对于 Connect 和 Destroy 操作,我们使用 link 和 cut 就好了。

4. 实现5

  至此,我们已经完全可以口胡通过上面的例题了。但是怎么写???

①Splay 结点

  我们将 LCT 中的所有结点都定义为 Splay 结点:

typedef struct SplayNode
{
bool flag; // reverse
SplayNode* ch[2];
SplayNode* parent;
SplayNode() : flag() {}
void pushdown()
{
if (flag)
{
std::swap(ch[0], ch[1]);
ch[0]->flag ^= 1;
ch[1]->flag ^= 1;
flag = false;
}
}
} Node;


  也就是说,LCT 中的 Splay 结点和普通的 Splay 结点没有太大差异,只是多了一个 parent 指针。但另外,可以发现 Splay 结点少了两个成员函数6
maintain
comp
,前者用于维护一个结点对应子树的 size,后者用来判断应该选择左子树还是右子树。

②parent 指针

  Splay 结点的 parent 指针有什么用呢?

  事实上,我们在维护 LCT 时并不真正地连接非偏爱边,而是直接将 parent 指针当作保存非偏爱边的容器。当一个点不是 Splay 的根结点时,parent 指针指向它在 Splay 中的父结点;当一个点是 Splay 的根结点时,parent 指针会被当作非偏爱边。如何判断 parent 指针是非偏爱边还是真正指向 Splay 中的父结点的指针呢?只需要判断这个点是不是 Splay 的根结点就好了:

int isRoot(Node* node)
{
return node->parent->ch[0] != node && node->parent->ch[1] != node;
}


③rotate 操作和 Splay 操作

  由于我们并不知道要操作的结点在 Splay 的中序遍历中到底是第几个元素,所以我们不能像以前的 Splay 那样从上往下旋转,我们只能从当前结点不断往上旋转,直到当前结点为 Splay 的根。

  首先考虑如何判断当前结点是它父结点的左儿子还是右儿子:

int trace(Node* node)
{
return node->parent->ch[1] == node; // 左 0 右 1
}


  然后我们考虑旋转。由于是自底向上的,所以我们定义 rotate(x)rotate(x) 为将 xx 旋转至 xx 的父结点的位置

void rotate(Node* r) // 旋转至 r 的父结点的位置
{
Node* father = r->parent; // 父结点
Node* grand = father->parent; // 爷结点
int d = trace(r);
if (!isRoot(father)) grand->ch[grand->ch[1] == father] = r; // 爷结点的儿子变成了 r
father->ch[d] = r->ch[d ^ 1];
father->ch[d]->parent = father; // r 的子结点的 parent 发生了变化
father->parent = r; // r 的父结点的 parent 发生了变化
r->ch[d ^ 1] = father;
r->parent = grand; // r 的父结点发生了变化
}


  在此过程中,除了过去需要维护的东西,我们还需要维护一个结点的父结点。父结点发生改变的有 33 个结点:被旋转的结点,它的某个子结点和它的父结点。

  同理,我们定义 Splay(x)Splay(x) 表示将 xx 旋转至它所在 Splay 的根结点

void pushdown(Node* r) // 下传根到 r 这条链上的标记
{
if (isRoot(r)) return void(r->pushdown());
pushdown(r->parent);
r->pushdown();
}
void splay(Node* r) // 旋转 r 至根
{
pushdown(r);
while (!isRoot(r)) // 从下往上旋转
{
Node* father = r->parent;
// 如果是一字型,先旋转上面;如果是之字形,先旋转下面
if (!isRoot(father)) rotate(trace(r) == trace(father) ? father : r);
rotate(r);
}
}


  如果需要将 Splay 对应序列反转(reverse),只需要调用:

void reverse(Node* r)
{
r->flag ^= 1;
}


  不过需要保证
r
为 Splay 的根结点,所以反转操作都需要紧跟在 splay 操作之后。

  需要注意的是,上面的参数全部都是指针类型的(
Node*
),而非指针的引用(
Node*&
)。为什么呢?我们在 LCT 中是这样保存结点的:

Node nodes[maxn];


  也就是说,结点之间的关系(
parent
ch
指针)可能会变,但是结点本身是不会改变的
,换句话说,变的是结点的内容,而结点的位置却没有改变,所以不需要(也不能)传入引用。

④access 操作

  在知道了如何进行 Splay 相关的操作后,问题也就变得简单了:

void access(int code) // 之前的 node->ch[1] 的 parent 没变
{
Node* pre = null;
Node* node = nodes + code;
while (node != null)
{
splay(node);
node->ch[1] = pre;
pre = node;
node = node->parent;
}
}


  我们保存上一条链的指针
pre
,一开始为
null
。我们每次首先对当前结点进行 Splay 操作,然后让它的右子树从之前的变成
pre
,相当于是接上了上一条链(我们是从下往上接的,上一条链指上次进行了操作的下面那条链,看代码吧),断开了之前的右子树。但是由于之前的右子树的
parent
并未改变(根据定义,非偏爱边从 Splay 的根结点指向 toptop 的相邻结点的 Splay 结点),所以我们不用管它。最后令
pre = node
node = node->parent
,往上跳即可。直到不能再往上跳(
node == null
),说明我们已经跳到了根结点的父结点(
null
),退出即可。

⑤makeroot 和 findroot 操作

  终于,我们写完了最底层的代码,所以下面的代码就不用解释了,参考前面的描述

void makeroot(int code) // 指定结点成为对应连通块在 LCT 中的根
{
Node* node = nodes + code;
access(code);
splay(node); // 注意这个点也是 Splay 的根
reverse(node);
}
int findroot(int code) // 寻找某个结点对应连通块在 LCT 中的根
{
Node* node = nodes + code;
access(code);
splay(node);
while (node->ch[0] != null)
node = node->ch[0];
return splay(node), node - nodes; // note
}


  需要注意要在最后对根结点进行 Splay 操作,以保证时间复杂度。

⑥link 和 cut 操作

  无需解释,参考前面的描述

void link(int x, int y)
{
makeroot(x); // x 已是 Splay 的根结点
(nodes + x)->parent = nodes + y;
}
void cut(int x, int y)
{
Node* node = nodes + y;
makeroot(x);
access(y);
splay(node);
(nodes + x)->parent = node->ch[0] = null;
}


⑦初始化

  LCT 的初始形态是怎样的呢?如果一开始树没有边,我们让每个结点都单独成为一个连通块即可。为了安全,我们为
null
结点实际分配内存。

LinkCutTree()
{
null = new Node;
null->parent = null->ch[0] = null->ch[1] = null;
}
void init()
{
for (int i = 1; i <= n; i++)
nodes[i].ch[0] = nodes[i].ch[1] = nodes[i].parent = null;
}


  完整代码见代码仓库。

5. 时间复杂度

  可以证明,使用 Splay 维护 LCT 的均摊时间复杂度是 O(nlogn)O(nlog⁡n) 的。很明显的是,LCT 常数巨大。

  具体的证明高深莫测,限于水平和偏重点,我就不证了,建议你也不要入坑去证。

进阶

e.g. Luogu 3203 弹飞绵羊

  分块经典题目,同时也是 LCT 经典题目。

  对于每一个位置,我们将它看作一个结点,另外我们虚拟一个 n+1n+1 号结点,表示一条就跳出去的情况。由于每一个点只能跳到唯一的位置,因此我们可以建成一棵树。这样,询问就等价于问每一个点到根结点的距离。

  如何使用 LCT 来搞呢?如果我们额外维护子树大小,我们就可以知道有多少个结点的中序遍历在当前结点的前面,也就可以做了。

①维护 size

  这道题需要维护子树大小,虽然子树大小不是 LCT 的必需品,但是它是做题的必需品。下面是新的结点定义:

struct Node
{
int size;
bool flag;
Node* parent;
Node* ch[2];
Node() : size() {}
void pushdown()
{
if (flag)
{
ch[0]->flag ^= 1;
ch[1]->flag ^= 1;
std::swap(ch[0], ch[1]);
flag = false;
}
}
void maintain()
{
size = ch[0]->size + ch[1]->size + 1;
}
void reverse()
{
flag ^= 1;
}
};


(把 reverse 写进
Node
中与写外面没有区别)

  最主要需要维护 size 的地方是 rotate 操作:

void rotate(Node* r)
{
Node* father = r->parent;
Node* grand = father->parent;
int d = trace(r);
if (!isRoot(father)) grand->ch[trace(father)] = r;
r->parent = grand;
father->ch[d] = r->ch[d ^ 1];
r->ch[d ^ 1]->parent = father;
father->maintain();
r->ch[d ^ 1] = father;
father->parent = r;
r->maintain();
}


  其它地方,仅当需要连接或者删去 Splay 中的边时,才需要维护 size。**由于 link 操作连接的是非偏爱边,所以不需要维护 size;**access 和 cut 操作需要断开或者连上 Splay 中的边,所以也需要维护 size:

void access(int code)
{
Node* pre = null;
Node* node = nodes + code;
while (node != null)
{
splay(node);
node->ch[1] = pre;
node->maintain();
pre = node;
node = node->parent;
}
}
void cut(int x, int y)
{
Node* node = nodes + y;
makeroot(x);
access(y);
splay(node);
(nodes + x)->parent = node->ch[0] = null;
node->maintain();
}


②获取结点在原树中的深度

  首先需要对原树的根结点进行 makeroot 操作,然后对需要计算的结点进行 access 和 splay 操作,这样它的左子树大小就是深度比自己浅的结点个数,正是本题需要求的东西。

int depth(int x)
{
makeroot(n);
access(x);
splay(nodes + x);
return (nodes + x)->ch[0]->size;
}


e.g. Luogu 3690 [模板] Link-Cut Tree

题目大意:给你一个森林,要你维护森林连通性,不保证删边或者加边操作合法。森林中的点有点权,有查询路径上点权异或和的询问,还要求支持单点修改点权。

  首先我们解决 link 操作不合法的问题,很简单,只需要用 findroot 判断是否在同一连通块就好了。

void link(int x, int y)
{
makeroot(x);
if (findroot(y) == x) return;
(nodes + x)->parent = nodes + y;
}


①解决 cut 的合法性问题

  cut 操作合法的先决条件是待操作结点要相邻。我们首先 makeroot(x)makeroot(x),然后检查 findroot(y)findroot(y) 是否为 xx,如果不是,显然它们不在同一连通块。否则我们 Splay(y)Splay(y)(注意在 findroot 中已经 access(y)access(y) 了),然后检查 xx 和 yy 的中序遍历之间是否有别的结点,只需要看 yy 的左子树大小是否为 11 即可。如果不需要维护大小呢?那就检查两个地方:一看 yy 的左儿子是不是 xx,二看 xx 是不是没有右儿子。检查之后我们才能 cut。

void cut(int x, int y)
{
makeroot(x);
if (findroot(y) != x) return;
Node* node = nodes + y;
splay(node);
if (node->ch[0] != nodes + x || (nodes + x)->ch[1] != null) return; // note
(nodes + x)->parent = node->ch[0] = null; // note
node->maintain();
}


  注意别写错了。

②维护异或和

  事实上,我们只需要维护 Splay 中对应子树的异或和就行了,而且只需要沿用之前那道题的做法,改一下 maintain 函数就好了

void maintain()
{
sum = weight ^ ch[0]->sum ^ ch[1]->sum;
}


  就是说,调用 maintain 函数的时机是一样的,只是 maintain 函数的内容变了。这启示我们,LCT 可以很方便地维护点上的信息

  相比之下,修改点权和查询点权异或和就很简单了,随便写写都能过。记住在 Splay 发生改变时进行 maintain 操作。

void change(int pos, int val)
{
access(pos);
Node* node = nodes + pos;
splay(node);
node->weight = val;
node->maintain();
}
int query(int x, int y)
{
makeroot(x);
access(y);
Node* node = nodes + y;
splay(node);
return node->sum;
}


e.g. Luogu 2387 [NOI 2014] 魔法森林

题目大意:给定一张 nn 个点 mm 条边的图,每条边有两个边权 aa 和 bb。找一条 11 到 nn 的路径使得 max{a}+max{b}max{a}+max{b} 最小,如果不存在输出 −1−1。

  很容易往最小生成树上面去想,然后考虑一个套路做法。我们枚举 aa 的最大权值,然后看对于每个 aa 答案是多少。具体地说,我们可以把边按照 aa 从小到大排序,依次加入图中,每次再看看 11 到 nn 的最优答案。

  如何看呢?我们直接令 aa 为当前的最大边权,然后找一条从 11 到 nn 的路径使得路径上最大的 bb 最小就好了。利用非法解不会最优的思想,我们可以用 SPFA 在 O(nm2)O(nm2) 的时间复杂度内解决这个问题。然后恭喜 TLE。(我不会打大优化,所以就不讨论如何优化这个算法了)

  我们考虑使用 LCT 来维护这个东西。

①边权转点权

  一个不可否认的事实是,LCT 真的没有办法维护边上的信息,所以我们要想个办法把边上的信息换到点上

  由于树有 nn 个点,n−1n−1 条边,所以我们自然会想到把边的信息放到儿子结点上,但是这在 LCT 中是行不通的,因为 LCT 本身可以换根,一条边连接的两个点的父子关系会发生变化

  正确的做法是为每一条边都虚拟一个结点,点权为原边权,而原点权为 00。加边时,我们从原来的两个端点分别向边对应的端点连边即可。

②维护最小生成树

  事实上,原问题已经被转换成了一个动态维护最小生成树的问题:只不过原始的动态维护最小生成树需要首先求一次 MST,但这道题是从零开始维护的。

  如何维护 MST 呢?稍微有点经验的人都知道,加入一条边时,如果两点不在同一连通块,那么直接连上就行;如果两点在同一连通块,那么一定会形成一个环。我们删去环上的最大边即可。

  如何使用 LCT 来维护呢?对于每个 Splay 结点,我们保存它所在子树的最大边权以及相应的编号即可。

struct Node
{
Node* parent;
Node* ch[2];
bool flag;
int weight;
int max;
int idx;
int maxIdx;
void pushdown()
{
if (flag)
{
std::swap(ch[0], ch[1]);
ch[0]->flag ^= 1;
ch[1]->flag ^= 1;
flag = false;
}
}
void maintain()
{
if (weight >= std::max(ch[0]->max, ch[1]->max))
{
max = weight;
maxIdx = idx;
}
else if (ch[0]->max > ch[1]->max)
{
max = ch[0]->max;
maxIdx = ch[0]->maxIdx;
}
else
{
max = ch[1]->max;
maxIdx = ch[1]->maxIdx;
}
}
void reverse() { flag ^= 1; }
};


  maintain 操作的执行时机和之前完全一样。剩下查询信息的代码就很简单了,善用 makeroot,access 和 splay 即可。详见代码仓库。

  最小生成树加边:

if (e.from == e.to) continue; // note
if (tree.findroot(e.from) != tree.findroot(e.to))
{
tree.link(e.from, n + i);
tree.link(e.to, n + i);
}
else
{
int code = tree.qMax(e.from, e.to) - n;
if (edges[code].b <= e.b) continue;
tree.cut(code + n, edges[code].from);
tree.cut(code + n, edges[code].to);
tree.link(n + i, e.from);
tree.link(n + i, e.to);
}
如果没有特殊说明,链指一条包含至少一个点的且深度单调的简单路径,即祖先后代链。
到目前为止,你可以认为相邻的结点就是 toptop 的父结点,但实际上 LCT 需要进行换根操作,原树是否有根无关紧要,所以说成相邻才是正确的。
如无特殊说明,对某个点进行 Splay 操作表示将它旋转至它所在的 Splay 的根结点。
不知道什么东西?你可以出门右转先学一下 Splay,不过不保证你学的代码能用在接下来 LCT 的实现上。
LCT 的实现有很多,选择最适合自己的就好。如果你不会具体实现,又觉得我太弱或者代码太丑,请跳过这一部分。我的代码在访问元素时将以指针为主。
这只是我的习惯,如果你并不习惯在 Splay 结点中直接写成员函数也没关系,我只是想告诉你 LCT 的 Splay 结点在不用维护别的数据的情况下不需要引入 size。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: