LCT(Link-Cut Tree),是一类最常见的用来解决动态树问题的工具。所谓动态树问题,大概就是给定一棵树,对其形态进行改变,并进行查询操作。LCT 通过将树划分成不同的链,并使用 Splay 来维护每一条链来解决这一类问题。我们通过一道板子题来说明。
例题 动态树问题 给出一棵树,每个节点有初始权值。进行四种操作:
- 查询 x 到 y 路径上的异或和,保证连通
- 连接 x 和 y,如果已经联通则忽略
- 断开 x 和 y 中间的边,保证相邻,不保证存在
- 将点 x 上的权值改成 y
分析 这是非常经典的 LCT 模板题。LCT将树划分成为不相交的链并使用 Splay 对每条链进行维护。最重要的是换根操作和 Access 操作。具体来说:我们按照每个节点的深度作为权值来构建 Splay。对于一棵 Splay 树中的节点:如果它是根节点,它的父亲就是原树上所在链的最上面一个节点的父亲(链父亲);否则它的父亲就是 Splay 中的父亲。如果它在 Splay 树上没有儿子,它的儿子就是 0。可能有多个不同节点的父亲是同一个,但是每个父亲的儿子(如果存在)只有一个,这被形象地称为,认父不认子。
Access 操作中,每次传入一个节点 u,将 u 和根节点放到一条链当中。而对于不在最短路径上的节点都不属于这条链。具体来说:从 u 开始,先把所有换很的标记从根节点到 u 全部下传(注意顺序),然后将 u splay 到 Splay 树的根节点,并断开 u 的右儿子,那么此时就没有比它更深的节点和它在同一条链上了。然后把再考虑它的父亲(因为是根节点所以是链父亲),把父亲 splay 到对应的 Splay 树的根节点,然后把父亲的右儿子置为它,也即把它所在的链和父亲所在的链合并起来。再对父亲进行同样的操作,直到到达根节点。
说了那么多,代码其实很简单。
void Access(int u) {
int pos = u, son = 0;
while(pos) {
Splay(pos), s[pos].ch[1] = son;
PushUp(pos), pos = s[son = pos].fa;
}
}
然后就是换根操作。注意到换根对相对位置的直接影响只有 u 到根节点的链,而其他链的 Splay 树都是不变的。所以用区间翻转的操作,直接将 u 到根节点的路径打通,然后把 u splay 到这条链的根,那么此时 u 一定不存在右儿子(因为它就是最深的了),那么在 u 上打一个交换左右子树的标记,那么 u 就没有了左子树,成为了这条链最浅的,那么它就成为了根。
这个代码只有一行
inline void MakeRoot(int u) { Access(u), Splay(u), s[u].tag ^= 1, PushDown(u); }
那么所有操作都很显然了,但是还是要注意普通 Splay 和 LCT 上 Splay 的不同,重点在于根节点的父亲不再没有意义,所以需要额外考察一个节点是不是根。
#include<algorithm>
#include<cstdio>
const int maxn = 1E+5 + 5;
const int INF = 0x3f3f3f3f;
int n, m;
struct SplayNode {
int Xor, val, tag;
int fa, ch[2];
} s[maxn];
inline bool nRoot(int u) { return s[s[u].fa].ch[0] == u || s[s[u].fa].ch[1] == u; }
inline bool Which(int u) { return s[s[u].fa].ch[1] == u; }
inline void PushUp(int u) { s[u].Xor = s[s[u].ch[0]].Xor ^ s[s[u].ch[1]].Xor ^ s[u].val; }
inline void PushDown(int u) {
if(s[u].tag) {
s[s[u].ch[0]].tag ^= 1, s[s[u].ch[1]].tag ^= 1;
std::swap(s[u].ch[0], s[u].ch[1]), s[u].tag = 0;
}
}
void Rotate(int u) {
bool Whi = Which(u), WhiFa = Which(s[u].fa);
int fa = s[u].fa, grand = s[fa].fa, son = s[u].ch[Whi ^ 1];
PushDown(fa), PushDown(u);
if(nRoot(fa)) s[grand].ch[WhiFa] = u;
if(son) s[son].fa = fa;
s[u].fa = grand, s[fa].ch[Whi] = son;
s[fa].fa = u, s[u].ch[Whi ^ 1] = fa;
PushUp(fa), PushUp(u);
}
int top, sta[maxn];
void Splay(int u) {
int pos = u; top = 0;
while(sta[++top] = pos, nRoot(pos)) pos = s[pos].fa;
while(top) PushDown(sta[top--]);
while(nRoot(u)) {
int fa = s[u].fa, grand = s[fa].fa;
if(nRoot(fa)) Rotate(Which(u) ^ Which(fa) ? u : fa);
Rotate(u);
}
}
void Access(int u) {
int pos = u, son = 0;
while(pos) {
Splay(pos), s[pos].ch[1] = son;
PushUp(pos), pos = s[son = pos].fa;
}
}
inline void MakeRoot(int u) { Access(u), Splay(u), s[u].tag ^= 1, PushDown(u); }
int FindRoot(int u) {
Access(u), Splay(u);
while(s[u].ch[0]) PushDown(u), u = s[u].ch[0];
Splay(u);
return u;
}
inline void Split(int u, int v) { MakeRoot(u), Access(v), Splay(v); }
inline void Link(int u, int v) {
MakeRoot(u);
if(FindRoot(v) != u)
s[u].fa = v;
}
inline void Cut(int u, int v) {
MakeRoot(u);
if(FindRoot(v) == u && s[v].fa == u && !s[v].ch[0])
s[v].fa = s[u].ch[1] = 0, PushUp(u);
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i)
scanf("%d", &s[i].val);
int opt, x, y;
while(m --> 0) {
scanf("%d%d%d", &opt, &x, &y);
if(opt == 0) Split(x, y), printf("%d
", s[y].Xor);
if(opt == 1) Link(x, y);
if(opt == 2) Cut(x, y);
if(opt == 3) Splay(x), s[x].val = y, PushUp(x);
}
}