Link Cut Tree
简介¶
- Link/Cut Tree 是一种数据结构,我们用它来解决动态树问题。
- Link/Cut Tree 又称 Link-Cut Tree,简称 LCT,但它不叫动态树,动态树是指一类问题。
- Splay Tree 是 LCT 的基础,但是 LCT 用的 Splay Tree 和普通的 Splay 在细节处不太一样(进行了一些扩展)。
- 这是一个和 Splay 一样只需要写几 (yi) 个 (dui) 核心函数就能实现一切的数据结构。
问题引入¶
- 维护一棵树,支持如下操作。
- 修改两点间路径权值。
- 查询两点间路径权值和。
- 修改某点子树权值。
- 查询某点子树权值和。 唔,看上去是一道树剖模版题。
那么我们加一个操作
- 断开并连接一些边,保证仍是一棵树。
要求在线求出上面的答案。
——动态树问题的解决方法:Link/Cut Tree!
动态树问题¶
- 维护一个森林, 支持删除某条边,加入某条边,并保证加边,删边之后仍是森林。我们要维护这个森林的一些信息。
- 一般的操作有两点连通性,两点路径权值和,连接两点和切断某条边、修改信息等。
从 LCT 的角度回顾一下树链剖分¶
- 对整棵树按子树大小进行剖分,并重新标号。
- 我们发现重新标号之后,在树上形成了一些以链为单位的连续区间,并且可以用线段树进行区间操作。
转向动态树问题¶
- 我们发现我们刚刚讲的树剖是以子树大小作为划分条件。
- 那我们能不能重定义一种剖分,使它更适应我们的动态树问题呢?
- 考虑动态树问题需要什么链。
- 由于动态维护一个森林,显然我们希望这个链是我们指定的链,以便利用来求解。
实链剖分¶
- 对于一个点连向它所有儿子的边 , 我们自己选择一条边进行剖分,我们称被选择的边为实边,其他边则为虚边。
- 对于实边,我们称它所连接的儿子为实儿子。
- 对于一条由实边组成的链,我们同样称之为实链。
- 请记住我们选择实链剖分的最重要的原因:它是我们选择的,灵活且可变。
- 正是它的这种灵活可变性,我们采用 Splay Tree 来维护这些实链。
LCT!¶
- 我们可以简单的把 LCT 理解成用一些 Splay 来维护动态的树链剖分,以期实现动态树上的区间操作。
- 对于每条实链,我们建一个 Splay 来维护整个链区间的信息。
- 接下来,我们来学习 LCT 的具体结构。
辅助树¶
- 我们先来看一看辅助树的一些性质,再通过一张图实际了解一下辅助树的具体结构。
-
在本文里,你可以认为一些 Splay 构成了一个辅助树,每棵辅助树维护的是一棵树,一些辅助树构成了 LCT,其维护的是整个森林。
-
辅助树由多棵 Splay 组成,每棵 Splay 维护原树中的一条路径,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树“从上到下”的一条路径。
- 原树每个节点与辅助树的 Splay 节点一一对应。
- 辅助树的各棵 Splay 之间并不是独立的。每棵 Splay 的根节点的父亲节点本应是空,但在 LCT 中每棵 Splay 的根节点的父亲节点指向原树中 这条链 的父亲节点(即链最顶端的点的父亲节点)。这类父亲链接与通常 Splay 的父亲链接区别在于儿子认父亲,而父亲不认儿子,对应原树的一条 虚边。因此,每个连通块恰好有一个点的父亲节点为空。
-
由于辅助树的以上性质,我们维护任何操作都不需要维护原树,辅助树可以在任何情况下拿出一个唯一的原树,我们只需要维护辅助树即可。(本句来源自 @PoPoQQQ 大爷的 PPT)
-
现在我们有一棵原树,如图。
- 加粗边是实边,虚线边是虚边。
- 由刚刚的定义,辅助树的结构如下
考虑原树和辅助树的结构关系¶
- 原树中的实链 : 在辅助树中节点都在一棵 Splay 中。
- 原树中的虚链 : 在辅助树中,子节点所在 Splay 的 Father 指向父节点,但是父节点的两个儿子都不指向子节点。
- 注意:原树的根不等于辅助树的根。
- 原树的 Father 指向不等于辅助树的 Father 指向。
- 辅助树是可以在满足辅助树、Splay 的性质下任意换根的。
- 虚实链变换可以轻松在辅助树上完成,这也就是实现了动态维护树链剖分。
接下来要用到的变量声明¶
ch[N][2]
左右儿子f[N]
父亲指向sum[N]
路径权值和val[N]
点权tag[N]
翻转标记laz[N]
权值标记siz[N]
辅助树上子树大小- Other_Vars
函数声明¶
一般数据结构函数(字面意思)¶
PushUp(x)
PushDown(x)
Splay 系函数(不会多做解释)¶
Get(x)
获取x Splay(x)
通过和 Rotate 操作联动实现把x Rotate(x)
将x
新操作¶
Access(x)
把从根到x x IsRoot(x)
判断x Update(x)
在Access
操作之后,递归地从上到下Pushdown
更新信息。MakeRoot(x)
使x Link(x, y)
在x, y Cut(x, y)
把x, y Find(x)
找到x Fix(x, v)
修改x v Split(x, y)
提取出x, y
宏定义¶
#define ls ch[p][0]
#define rs ch[p][1]
函数讲解¶
先从简单的来吧
PushUp()
¶
1 2 3 4 | inline void PushUp(int p) {
// maintain other variables
siz[p] = siz[ls] + siz[rs] + 1;
}
|
PushDown()
¶
1 2 3 4 5 6 | inline void PushDown(int p) {
if (tag[p] != std_tag) {
// pushdown the tag
tag[p] = std_tag;
}
}
|
Splay() && Rotate()
¶
有些不一样了哦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #define Get(x) (ch[f[x]][1] == x)
inline void Rotate(int x) {
int y = f[x], z = f[y], k = Get(x);
if (!isRoot(y)) ch[z][ch[z][1] == y] = x;
// 上面这句一定要写在前面,普通的Splay是不用的,因为 isRoot (后面会讲)
ch[y][k] = ch[x][!k], f[ch[x][!k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
PushUp(y), PushUp(x);
}
inline void Splay(int x) {
Update(
x); // 马上就能看到啦。 在 Splay之前要把旋转会经过的路径上的点都PushDown
for (int fa; fa = f[x], !isRoot(x); Rotate(x)) {
if (!isRoot(fa)) Rotate(Get(fa) == Get(x) ? fa : x);
}
}
|
如果上面的几个函数你看不懂,请移步 Splay。
下面要开始 LCT 独有的函数了哦。
isRoot()
¶
1 2 3 | // 在前面我们已经说过,LCT 具有 如果一个儿子不是实儿子,他的父亲找不到它的性质
// 所以当一个点既不是它父亲的左儿子,又不是它父亲的右儿子,它就是当前 Splay 的根
#define isRoot(x) (ch[f[x]][0] != x && ch[f[x]][1] != x)
|
Access()
¶
1 2 3 4 5 6 7 8 9 10 | // Access 是 LCT
// 的核心操作,试想我们像求解一条路径,而这条路径恰好就是我们当前的一棵 Splay,
// 直接调用其信息即可。先来看一下代码,再结合图来看看过程
inline int Access(int x) {
int p;
for (p = 0; x; p = x, x = f[x]) {
Splay(x), ch[x][1] = p, PushUp(x);
}
return p;
}
|
-
我们有这样一棵树,实线为实边,虚线为虚边。
-
它的辅助树可能长成这样(构图方式不同可能 LCT 的结构也不同)。
-
现在我们要
Access(N)
, 把A N -
实现的方法是从下到上逐步更新 Splay。
-
首先我们要把
N -
为了保证 AuxTree(辅助树)的性质,原来
N O -
由于认父不认子的性质,我们可以单方面的把
N -
于是原来的 AuxTree 就从下图变成了下下图。
-
下一步,我们把
N I I -
原来的实边
I K I N I L -
接下来,按照刚刚的操作步骤,由于
I H H H I -
之后的树是这样的。
-
同理我们
Splay(A)
, 并把A H -
于是我们得到了这样一棵 AuxTree。并且发现
A N
1 2 3 4 5 6 7 8 | // 回顾一下代码
inline int Access(int x) {
int p;
for (p = 0; x; p = x, x = f[x]) {
Splay(x), ch[x][1] = p, PushUp(x);
}
return p;
}
|
我们发现 Access()
其实很容易,只有如下四步操作:
- 把当前节点转到根。
- 把儿子换成之前的节点。
- 更新当前点的信息。
- 把当前点换成当前点的父亲,继续操作。
这里提供的 Access 还有一个返回值。这个返回值相当于最后一次虚实链变换时虚边父亲节点的编号。该值有两个含义:
- 连续两次 Access 操作时,第二次 Access 操作的返回值等于这两个节点的 LCA.
- 表示
x
Update()
¶
1 2 3 4 5 | // 从上到下一层一层 pushDown 即可
void Update(int p) {
if (!isRoot(p)) Update(f[p]);
pushDown(p);
}
|
makeRoot()
¶
Make_Root()
的重要性丝毫不亚于Access()
。我们在需要维护路径信息的时候,一定会出现路径深度无法严格递增的情况,根据 AuxTree 的性质,这种路径是不能出现在一棵 Splay 中的。- 这时候我们需要用到
Make_Root()
。 Make_Root()
的作用是使指定的点成为原树的根,考虑如何实现这种操作。- 设
Access(x)
的返回值为y x y - 考虑将树用有向图表示出来,给每条边定一个方向,表示从儿子到父亲的方向。容易发现换根相当于将
x - 因此将
x - 由于
y x y
1 2 3 4 5 | inline void makeRoot(int p) {
p = Access(p);
swap(ch[p][0], ch[p][1]);
tag[p] ^= 1;
}
|
Link()
¶
- Link 两个点其实很简单,先
Make_Root(x)
, 然后把x y
1 2 3 4 5 | inline void Link(int x, int p) {
makeRoot(x);
splay(x);
f[x] = p;
}
|
Split()
¶
Split
操作意义很简单,就是拿出一棵 Splay , 维护的是x y - 先
MakeRoot(x)
,然后Access(y)
。如果要y Splay(y)
。 - 就这三句话,没写代码,需要的时候可以直接打这三个就好辣!
- 另外 Split 这三个操作直接可以把需要的路径拿出到
y
Cut()
¶
Cut
有两种情况,保证合法和不一定保证合法。(废话)- 如果保证合法,直接
Split(x, y)
,这时候y x
1 2 3 | inline void Cut(int x, int p) {
makeRoot(x), Access(p), Splay(p), ls = f[x] = 0;
}
|
如果是不保证合法,我们需要判断一下是否有,我选择使用 map
存一下,但是这里有一个利用性质的方法:
想要删边,必须要满足如下三个条件:
x,y x,y x
总结一下,上面三句话的意思就一个:
具体实现就留作一个思考题给大家。判断连通需要用到后面的 Find
, 其他两点稍作思考分析一下结构就知道该怎么判断了。
Find()
¶
Find()
其实就是找到当前辅助树的根。在Access(p)
后,再Splay(p)
。这样根就是树里最小的那个,一直往 ls 走,沿途PushDown
即可。- 一直走到没有 ls, 非常简单。
- 注意,每次查询之后需要把查询到的答案对应的结点
Splay
上去以保证复杂度。
1 2 3 4 5 6 | inline int Find(int p) {
Access(p), Splay(p), pushDown(p);
while (ls) p = ls, pushDown(p);
Splay(p);
return p;
}
|
一些提醒¶
- 干点啥前一定要想一想需不需要
PushUp
或者PushDown
, LCT 由于特别灵活的原因,少Pushdown
或者Pushup
一次就可能把修改改到不该改的点上! - LCT 的
Rotate
和 Splay 的不太一样,if (z)
一定要放在前面。 - LCT 的
Splay
操作就是旋转到根,没有旋转到谁儿子的操作,因为不需要。
习题¶
维护树链信息¶
LCT 通过 Split(x,y)
操作,可以将树上从点
例题「国家集训队」Tree II
给出一棵有
- u1 v1 u2 v2
:将树上u_1,v_1 u_2,v_2 + u v c
:将树上u,v c * u v c
:将树上u,v c -
/ u v
:输出树上u,v 51061 1\le n,q\le 10^5,0\le c\le 10^4 -
操作可以直接Cut(u1,v1),Link(u2,v2)
。
对树上 Split(u,v)
。
此题要求进行在辅助树上的子树加,子树乘,子树求和操作,所以我们除了一般 LCT 需要维护的子树翻转标记,还要维护子树加法标记和子树乘法标记。处理标记的方法和在 Splay 上是一样的。
在打上和下传加法标记时,子树权值和的变化量和子树中的结点数有关,所以我们还要维护子树的大小 siz
。
在下传标记时,需要注意顺序,先下传乘法标记再下传加法标记。子树翻转和子树加乘两种标记没有冲突。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | #include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
#define int long long
const int maxn = 100010;
const int mod = 51061;
int n, q, u, v, c;
char op;
struct Splay {
int ch[maxn][2], fa[maxn], siz[maxn], val[maxn], sum[maxn], rev[maxn],
add[maxn], mul[maxn];
void clear(int x) {
ch[x][0] = ch[x][1] = fa[x] = siz[x] = val[x] = sum[x] = rev[x] = add[x] =
0;
mul[x] = 1;
}
int getch(int x) { return (ch[fa[x]][1] == x); }
int isroot(int x) {
clear(0);
return ch[fa[x]][0] != x && ch[fa[x]][1] != x;
}
void maintain(int x) {
clear(0);
siz[x] = (siz[ch[x][0]] + 1 + siz[ch[x][1]]) % mod;
sum[x] = (sum[ch[x][0]] + val[x] + sum[ch[x][1]]) % mod;
}
void pushdown(int x) {
clear(0);
if (mul[x] != 1) {
if (ch[x][0])
mul[ch[x][0]] = (mul[x] * mul[ch[x][0]]) % mod,
val[ch[x][0]] = (val[ch[x][0]] * mul[x]) % mod,
sum[ch[x][0]] = (sum[ch[x][0]] * mul[x]) % mod,
add[ch[x][0]] = (add[ch[x][0]] * mul[x]) % mod;
if (ch[x][1])
mul[ch[x][1]] = (mul[x] * mul[ch[x][1]]) % mod,
val[ch[x][1]] = (val[ch[x][1]] * mul[x]) % mod,
sum[ch[x][1]] = (sum[ch[x][1]] * mul[x]) % mod,
add[ch[x][1]] = (add[ch[x][1]] * mul[x]) % mod;
mul[x] = 1;
}
if (add[x]) {
if (ch[x][0])
add[ch[x][0]] = (add[ch[x][0]] + add[x]) % mod,
val[ch[x][0]] = (val[ch[x][0]] + add[x]) % mod,
sum[ch[x][0]] = (sum[ch[x][0]] + add[x] * siz[ch[x][0]] % mod) % mod;
if (ch[x][1])
add[ch[x][1]] = (add[ch[x][1]] + add[x]) % mod,
val[ch[x][1]] = (val[ch[x][1]] + add[x]) % mod,
sum[ch[x][1]] = (sum[ch[x][1]] + add[x] * siz[ch[x][1]] % mod) % mod;
add[x] = 0;
}
if (rev[x]) {
if (ch[x][0]) rev[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]);
if (ch[x][1]) rev[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]);
rev[x] = 0;
}
}
void update(int x) {
if (!isroot(x)) update(fa[x]);
pushdown(x);
}
void print(int x) {
if (!x) return;
pushdown(x);
print(ch[x][0]);
printf("%lld ", x);
print(ch[x][1]);
}
void rotate(int x) {
int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y)) ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
maintain(y);
maintain(x);
maintain(z);
}
void splay(int x) {
update(x);
for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = fa[x]) splay(x), ch[x][1] = f, maintain(x);
}
void makeroot(int x) {
access(x);
splay(x);
swap(ch[x][0], ch[x][1]);
rev[x] ^= 1;
}
int find(int x) {
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
} st;
main() {
scanf("%lld%lld", &n, &q);
for (int i = 1; i <= n; i++) st.val[i] = 1, st.maintain(i);
for (int i = 1; i < n; i++) {
scanf("%lld%lld", &u, &v);
if (st.find(u) != st.find(v)) st.makeroot(u), st.fa[u] = v;
}
while (q--) {
scanf(" %c%lld%lld", &op, &u, &v);
if (op == '+') {
scanf("%lld", &c);
st.makeroot(u), st.access(v), st.splay(v);
st.val[v] = (st.val[v] + c) % mod;
st.sum[v] = (st.sum[v] + st.siz[v] * c % mod) % mod;
st.add[v] = (st.add[v] + c) % mod;
}
if (op == '-') {
st.makeroot(u);
st.access(v);
st.splay(v);
if (st.ch[v][0] == u && !st.ch[u][1]) st.ch[v][0] = st.fa[u] = 0;
scanf("%lld%lld", &u, &v);
if (st.find(u) != st.find(v)) st.makeroot(u), st.fa[u] = v;
}
if (op == '*') {
scanf("%lld", &c);
st.makeroot(u), st.access(v), st.splay(v);
st.val[v] = st.val[v] * c % mod;
st.sum[v] = st.sum[v] * c % mod;
st.mul[v] = st.mul[v] * c % mod;
}
if (op == '/')
st.makeroot(u), st.access(v), st.splay(v), printf("%lld\n", st.sum[v]);
}
return 0;
}
|
习题¶
维护连通性质¶
判断是否连通¶
借助 LCT 的 Find()
函数,可以判断动态森林上的两点是否连通。如果有 Find(x)==Find(y)
,则说明
例题「SDOI2008」洞穴勘测
一开始有
Connect u v
:在u,v Destroy u v
:删除在u,v Query u v
:询问u,v
保证在任何时刻图的形态都是一个森林。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | #include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 10010;
struct Splay {
int ch[maxn][2], fa[maxn], tag[maxn];
void clear(int x) { ch[x][0] = ch[x][1] = fa[x] = tag[x] = 0; }
int getch(int x) { return ch[fa[x]][1] == x; }
int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; }
void pushdown(int x) {
if (tag[x]) {
if (ch[x][0]) swap(ch[ch[x][0]][0], ch[ch[x][0]][1]), tag[ch[x][0]] ^= 1;
if (ch[x][1]) swap(ch[ch[x][1]][0], ch[ch[x][1]][1]), tag[ch[x][1]] ^= 1;
tag[x] = 0;
}
}
void update(int x) {
if (!isroot(x)) update(fa[x]);
pushdown(x);
}
void rotate(int x) {
int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y)) ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
}
void splay(int x) {
update(x);
for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = fa[x]) splay(x), ch[x][1] = f;
}
void makeroot(int x) {
access(x);
splay(x);
swap(ch[x][0], ch[x][1]);
tag[x] ^= 1;
}
int find(int x) {
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
} st;
int n, q, x, y;
char op[maxn];
int main() {
scanf("%d%d", &n, &q);
while (q--) {
scanf("%s%d%d", op, &x, &y);
if (op[0] == 'Q') {
if (st.find(x) == st.find(y))
printf("Yes\n");
else
printf("No\n");
}
if (op[0] == 'C')
if (st.find(x) != st.find(y)) st.makeroot(x), st.fa[x] = y;
if (op[0] == 'D') {
st.makeroot(x);
st.access(y);
st.splay(y);
if (st.ch[y][0] == x && !st.ch[x][1]) st.ch[y][0] = st.fa[x] = 0;
}
}
return 0;
}
|
维护边双连通分量¶
如果要求将边双连通分量缩成点,每次添加一条边,所连接的树上的两点如果相互连通,那么这条路径上的所有点都会被缩成一个点。
例题「AHOI2005」航线规划
给出
0 u v
:删除u,v 1 u v
:查询此时u,v
保证图在任意时刻都连通。
可以发现,
由于题目中的删边操作不好进行,我们考虑离线逆向进行操作,改删边为加边。
加入一条边时,如果两点原来不连通,则在 LCT 上连接两点;否则提取出加这条边之前 LCT 上这两点之间的路径,遍历辅助树上的这个子树,相当于遍历了这条路径,将这些点合并,利用并查集维护合并的信息。
用合并后并查集的代表元素代替原来树上的路径。注意之后的每次操作都要找到操作点在并查集上的代表元素进行操作。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | #include <algorithm>
#include <cstdio>
#include <cstring>
#include <map>
using namespace std;
const int maxn = 200010;
int f[maxn];
int findp(int x) { return f[x] ? f[x] = findp(f[x]) : x; }
void merge(int x, int y) {
x = findp(x);
y = findp(y);
if (x != y) f[x] = y;
}
struct Splay {
int ch[maxn][2], fa[maxn], tag[maxn], siz[maxn];
void clear(int x) { ch[x][0] = ch[x][1] = fa[x] = tag[x] = siz[x] = 0; }
int getch(int x) { return ch[findp(fa[x])][1] == x; }
int isroot(int x) {
return ch[findp(fa[x])][0] != x && ch[findp(fa[x])][1] != x;
}
void maintain(int x) {
clear(0);
if (x) siz[x] = siz[ch[x][0]] + 1 + siz[ch[x][1]];
}
void pushdown(int x) {
if (tag[x]) {
if (ch[x][0]) tag[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]);
if (ch[x][1]) tag[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]);
tag[x] = 0;
}
}
void print(int x) {
if (!x) return;
pushdown(x);
print(ch[x][0]);
printf("%d ", x);
print(ch[x][1]);
}
void update(int x) {
if (!isroot(x)) update(findp(fa[x]));
pushdown(x);
}
void rotate(int x) {
x = findp(x);
int y = findp(fa[x]), z = findp(fa[y]), chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y)) ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
maintain(y);
maintain(x);
if (z) maintain(z);
}
void splay(int x) {
x = findp(x);
update(x);
for (int f = findp(fa[x]); f = findp(fa[x]), !isroot(x); rotate(x))
if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = findp(fa[x]))
splay(x), ch[x][1] = f, maintain(x);
}
void makeroot(int x) {
x = findp(x);
access(x);
splay(x);
tag[x] ^= 1;
swap(ch[x][0], ch[x][1]);
}
int find(int x) {
x = findp(x);
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
void dfs(int x) {
pushdown(x);
if (ch[x][0]) dfs(ch[x][0]), merge(ch[x][0], x);
if (ch[x][1]) dfs(ch[x][1]), merge(ch[x][1], x);
}
} st;
int n, m, q, x, y, cur, ans[maxn];
struct oper {
int op, a, b;
} s[maxn];
map<pair<int, int>, int> mp;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) st.maintain(i);
for (int i = 1; i <= m; i++)
scanf("%d%d", &x, &y), mp[{x, y}] = mp[{y, x}] = 1;
while (scanf("%d", &s[++q].op)) {
if (s[q].op == -1) {
q--;
break;
}
scanf("%d%d", &s[q].a, &s[q].b);
if (!s[q].op) mp[{s[q].a, s[q].b}] = mp[{s[q].b, s[q].a}] = 0;
}
reverse(s + 1, s + q + 1);
for (map<pair<int, int>, int>::iterator it = mp.begin(); it != mp.end(); it++)
if (it->second) {
mp[{it->first.second, it->first.first}] = 0;
x = findp(it->first.first);
y = findp(it->first.second);
if (st.find(x) != st.find(y))
st.makeroot(x), st.fa[x] = y;
else {
if (x == y) continue;
st.makeroot(x);
st.access(y);
st.splay(y);
st.dfs(y);
int t = findp(y);
st.fa[t] = findp(st.fa[y]);
st.ch[t][0] = st.ch[t][1] = 0;
st.maintain(t);
}
}
for (int i = 1; i <= q; i++) {
if (s[i].op == 0) {
x = findp(s[i].a);
y = findp(s[i].b);
st.makeroot(x);
st.access(y);
st.splay(y);
st.dfs(y);
int t = findp(y);
st.fa[t] = st.fa[y];
st.ch[t][0] = st.ch[t][1] = 0;
st.maintain(t);
}
if (s[i].op == 1) {
x = findp(s[i].a);
y = findp(s[i].b);
st.makeroot(x);
st.access(y);
st.splay(y);
ans[++cur] = st.siz[y] - 1;
}
}
for (int i = cur; i >= 1; i--) printf("%d\n", ans[i]);
return 0;
}
|
习题¶
维护边权¶
LCT 并不能直接处理边权,此时需要对每条边建立一个对应点,方便查询链上的边信息。利用这一技巧可以动态维护生成树。
例题luogu P4234 最小差值生成树
给定一个
数据保证至少存在一棵生成树。
将边按照边权从小到大排序,枚举选择的最右边的一条边,要得到最优解,需要使边权最小边的边权最大。
每次按照顺序添加边,如果将要连接的这两个点已经连通,则删除这两点之间边权最小的一条边。如果整个图已经连通成了一棵树,则用当前边权减去最小边权更新答案。最小边权可用双指针法更新。
LCT 上没有固定的父子关系,所以不能将边权记录在点权中。
记录树链上的边的信息,可以使用 拆边。对每条边建立一个对应的点,从这条边向其两个端点连接一条边,原先的连边与删边操作都变成两次操作。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | #include <algorithm>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;
const int maxn = 5000010;
struct Splay {
int ch[maxn][2], fa[maxn], tag[maxn], val[maxn], minn[maxn];
void clear(int x) {
ch[x][0] = ch[x][1] = fa[x] = tag[x] = val[x] = minn[x] = 0;
}
int getch(int x) { return ch[fa[x]][1] == x; }
int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; }
void maintain(int x) {
if (!x) return;
minn[x] = x;
if (ch[x][0]) {
if (val[minn[ch[x][0]]] < val[minn[x]]) minn[x] = minn[ch[x][0]];
}
if (ch[x][1]) {
if (val[minn[ch[x][1]]] < val[minn[x]]) minn[x] = minn[ch[x][1]];
}
}
void pushdown(int x) {
if (tag[x]) {
if (ch[x][0]) tag[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]);
if (ch[x][1]) tag[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]);
tag[x] = 0;
}
}
void update(int x) {
if (!isroot(x)) update(fa[x]);
pushdown(x);
}
void print(int x) {
if (!x) return;
pushdown(x);
print(ch[x][0]);
printf("%d ", x);
print(ch[x][1]);
}
void rotate(int x) {
int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y)) ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
maintain(y);
maintain(x);
if (z) maintain(z);
}
void splay(int x) {
update(x);
for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = fa[x]) splay(x), ch[x][1] = f, maintain(x);
}
void makeroot(int x) {
access(x);
splay(x);
tag[x] ^= 1;
swap(ch[x][0], ch[x][1]);
}
int find(int x) {
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
void link(int x, int y) {
makeroot(x);
fa[x] = y;
}
void cut(int x, int y) {
makeroot(x);
access(y);
splay(y);
ch[y][0] = fa[x] = 0;
maintain(y);
}
} st;
const int inf = 2e9 + 1;
int n, m, ans, nww, x, y;
struct Edge {
int u, v, w;
bool operator<(Edge x) const { return w < x.w; };
} s[maxn];
multiset<int> mp;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) st.val[i] = inf, st.maintain(i);
for (int i = 1; i <= m; i++) scanf("%d%d%d", &s[i].u, &s[i].v, &s[i].w);
sort(s + 1, s + m + 1);
for (int i = 1; i <= m; i++) st.val[n + i] = s[i].w, st.maintain(n + i);
for (int i = 1; i <= m; i++) {
x = s[i].u;
y = s[i].v;
if (x == y) continue;
if (st.find(x) != st.find(y)) {
nww++;
st.link(x, n + i);
st.link(n + i, y);
mp.insert(s[i].w);
if (nww == n - 1) ans = s[i].w - (*(mp.begin()++));
} else {
st.makeroot(x);
st.access(y);
st.splay(y);
int t = st.minn[y] - n;
st.cut(s[t].u, t + n);
st.cut(t + n, s[t].v);
mp.erase(mp.find(s[t].w));
st.link(x, n + i);
st.link(n + i, y);
mp.insert(s[i].w);
if (nww == n - 1) ans = min(ans, s[i].w - (*(mp.begin()++)));
}
}
printf("%d\n", ans);
return 0;
}
|
习题¶
维护子树信息¶
LCT 不擅长维护子树信息。统计一个结点所有虚子树的信息,就可以求得整棵树的信息。
例题「BJOI2014」大融合
给定
A x y
在结点x y Q x y
给定一条已经存在的边(x,y) (x,y)
保证在任意时刻,图的形态都是一棵森林。
为询问 Q
考虑另一种表述,我们发现答案等于边
题目中的操作既有连边,又有删边,还保证在任意时刻都是一棵森林,我们不由得想到用 LCT 来维护。但是这题中 LCT 维护的是子树的大小,不像我们印象中的维护一条链的信息,而 LCT 的构造 认父不认子,不方便我们直接进行子树的统计。怎么办呢?
方法是统计一个结点
定义
不同于以往我们维护 Splay 中子树结点个数的方法,我们在计算结点
1 2 3 4 | void maintain(int x) {
clear(0);
if (x) siz[x] = siz[ch[x][0]] + 1 + siz[ch[x][1]] + siz2[x];
}
|
而且在我们 改变 Splay 的形态(即改变一个结点在 Splay 上的左右儿子指向时),需要及时修改
在 Rotate(),Splay()
操作中,我们都只是改变了 Splay 中结点的相对位置,没有改变任意一条边的虚实情况,所以不对
在 access
操作中,在每次 splay 完后,都会改变刚刚 splay 完的结点的右儿子,即该结点与其原右儿子的连边和该节点和新右儿子的连边的虚实情况发生了变化,我们需要加上新变成虚边所连的子树的贡献,减去刚刚变成实边所连的子树的贡献。代码如下:
1 2 3 4 | void access(int x) {
for (int f = 0; x; f = x, x = fa[x])
splay(x), siz2[x] += siz[ch[x][1]] - siz[f], ch[x][1] = f, maintain(x);
}
|
在 MakeRoot(),Find()
操作中,我们都只是调用了之前的函数或者在 Splay 上条边,并不用做任何修改。
在连接两点时,我们修改了一个结点的父亲。我们需要在父亲结点的
1 2 3 4 | st.makeroot(x);
st.makeroot(y);
st.fa[x] = y;
st.siz2[y] += st.siz[x];
|
在断开一条边时,我们只是删除了 Splay 上的一条实边,Maintain
操作会维护这些信息,不需要做任何修改。
代码修改的细节讲完了,总结一下 LCT 维护子树信息的要求与方法:
- 维护的信息要有 可减性,如子树结点数,子树权值和,但不能直接维护子树最大最小值,因为在将一条虚边变成实边时要排除原先虚边的贡献。
- 新建一个附加值存储虚子树的贡献,在统计时将其加入本结点答案,在改变边的虚实时及时维护。
- 其余部分同普通 LCT,在统计子树信息时一定将其作为根节点。
- 如果维护的信息没有可减性,如维护区间最值,可以对每个结点开一个平衡树维护结点的虚子树中的最值。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | #include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 100010;
typedef long long ll;
struct Splay {
int ch[maxn][2], fa[maxn], siz[maxn], siz2[maxn], tag[maxn];
void clear(int x) {
ch[x][0] = ch[x][1] = fa[x] = siz[x] = siz2[x] = tag[x] = 0;
}
int getch(int x) { return ch[fa[x]][1] == x; }
int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; }
void maintain(int x) {
clear(0);
if (x) siz[x] = siz[ch[x][0]] + 1 + siz[ch[x][1]] + siz2[x];
}
void pushdown(int x) {
if (tag[x]) {
if (ch[x][0]) swap(ch[ch[x][0]][0], ch[ch[x][0]][1]), tag[ch[x][0]] ^= 1;
if (ch[x][1]) swap(ch[ch[x][1]][0], ch[ch[x][1]][1]), tag[ch[x][1]] ^= 1;
tag[x] = 0;
}
}
void update(int x) {
if (!isroot(x)) update(fa[x]);
pushdown(x);
}
void rotate(int x) {
int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y)) ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
maintain(y);
maintain(x);
maintain(z);
}
void splay(int x) {
update(x);
for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = fa[x])
splay(x), siz2[x] += siz[ch[x][1]] - siz[f], ch[x][1] = f, maintain(x);
}
void makeroot(int x) {
access(x);
splay(x);
swap(ch[x][0], ch[x][1]);
tag[x] ^= 1;
}
int find(int x) {
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0];
splay(x);
return x;
}
} st;
int n, q, x, y;
char op;
int main() {
scanf("%d%d", &n, &q);
while (q--) {
scanf(" %c%d%d", &op, &x, &y);
if (op == 'A') {
st.makeroot(x);
st.makeroot(y);
st.fa[x] = y;
st.siz2[y] += st.siz[x];
}
if (op == 'Q') {
st.makeroot(x);
st.access(y);
st.splay(y);
st.ch[y][0] = st.fa[x] = 0;
st.maintain(x);
st.makeroot(x);
st.makeroot(y);
printf("%lld\n", (ll)(st.siz[x] * st.siz[y]));
st.makeroot(x);
st.makeroot(y);
st.fa[x] = y;
st.siz2[y] += st.siz[x];
}
}
return 0;
}
|
习题¶
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用