等下发树剖的学习笔记
LCT(link—cut—tree) 是解决一类动态树问题的数据结构
主要是给一个有根树的森林,然后有动态插入边,删除边,询问等操作
保证时刻是一个森林
LCT维护子树信息比较麻烦,这里暂时不提
一、实边和虚边:
LCT 会将儿子划分为虚、实两种儿子,
相应的边称为虚边或实边,且任意时刻一个节点最多只会有一个实儿子(可能没有)。由于树的形态会改变,因此 LCT 不是严格的划分虚实儿子,而是动态地改变,它使用了数据结构来维护实链(连续的实边),并且用的是灵活的 Splay.
二、实路径和path_parent:
实边连成实路径,且不可伸长。
一个实边的path_parent是指这个实边的最浅节点的父亲,用这个性质可以完成许多操作。
三、splay
维护实路径可以用splay,因为实路径上任意两个点都是祖先与子孙的关系。换句话说,如果用深度作为关键字给结点排序,那么我们将得到一个唯一的有序结点序列。平衡树中每个结点的左子树中结点在实路径中的深度都小于该点,右子树中的都大于该结点,因此平衡树的最左结点对应该路径的头部,最右结点对应该路径的尾部。
四、基本操作:
※1.access(x)
这个是最重要的操作:以 x 为起点,一直到根节点,构造出一条链。
该操作将 x 到根结点的路径上的所有边都变为实边, 当然,为了保持实边、虚边划分的性质,一部分原来的实边也要相应变为虚边。注意该操作会将 x下方的实边变为虚边。该操作的步骤如下:
(1) 如果结点 x 不是其所在实路径的尾部, 即 x有子结点与之用实边相连, 那么需要 “断开”这条边(断开并不是将这条边删除,而只是将其转变为虚边)。方法是首先将结点 x用 Splay操作旋转到所在平衡树的根结点,然后 x 肯定有右子树,故将 x 与 x的右子树分离,同时将 x 的右子树的 Path_Parent 设置为 x。
(2) 如果结点 x所在的平衡树包含根结点,那么该过程结束;否则,转步骤(3);
(3) 设 y 为 x 所在平衡树的 Path_Parent。将 y 用 Splay 操作旋转到其所属平衡树的根结点,并且用 x 所在的平衡树替换 y的右子树,这样就实现了实路径的向上延伸。当然到这里还没有结束,我们需要分离原来 y 的右子树,y原来的右孩子记为 P。此时 P 的 Path_Parent就为 y了,然后继续转步骤(2)。
实现时,我们可以把splay中根节点的fa改为path_parent,这样每个点的父亲,要么是实路径上的点,要么是它所处实路径的Path_Parent,特别地,这棵树的根节点的 Path_Parent 为 0。这样的话一个点,它的 fa的左右儿子可能都不为它,因此判断根节点的条件也需要稍微修改一下。
2.findroot(x):找到x所在的树的根节点
我们access(x),然后x就和它的根节点在同一棵平衡树里了,那么根节点就是实路径的头部。
3.makeroot(x):使节点x成为其所在树的根节点
其实就是把x到根的路径取反。
先access(x),然后给splay的根节点打上翻转标记,就可以将x到根的路径取反了。
4.link(x,y)连接x,y这条边
先makeroot(x),再将x的fa变成y就好了
5.cut(x,y)删除一条边
先把x变成根节点makeroot(x),这样y就是x的子节点,然后就可以access(y),x和y就在同一棵平衡树里了。然后对 y 执行 Splay 操作使其
成为所在平衡树的根结点,同时分离 y和 y 的左子树,便完成了边的删除操作。
6.Select(x,y):取出树中(x,y)所在的路径,进而执行路径修改、路径询问等操作。
执行一次 MakeRoot(x)将 x 置为其所在树的根,再执行一次 Access(y)操作,就将 x 与 y以及它们路径上的所有点整合至同一棵平衡树中了。 此时我们在平衡树的根节点上打上标记或者直接查询信息即可。
五.例题:
BZOJ2049
传送门
代码:
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1e4 + 3;
int n, m, u, v, qr, que[N];
struct node {
int fa, lc, rc, rev;
#define fa(x) t[x].fa
#define lc(x) t[x].lc
#define rc(x) t[x].rc
#define rev(x) t[x].rev
} t[N];
inline int which(const int &x) {return rc(fa(x)) == x;}
inline bool isRoot(const int &x) {
if (!fa(x)) return true;
return lc(fa(x)) != x && rc(fa(x)) != x;
// 若fa=0 则 x 是原树中的根
// 若x 不是其父亲 Splay 中的儿子,则它是其所属实路径 Splay 的根
}
inline void downdate(const int &x) {
if (rev(x)) {
swap(lc(x), rc(x));
if (lc(x)) rev(lc(x)) ^= 1;
if (rc(x)) rev(rc(x)) ^= 1;
rev(x) = 0;
}
return ;
}
inline void Rotate(const int &x) {
int y = fa(x), z = fa(y), b = lc(y) == x ? rc(x) : lc(x);
if (z && !isRoot(y)) (lc(z) == y ? lc(z) : rc(z)) = x;
fa(x) = z; fa(y) = x; b ? fa(b) = y : 0;
if (lc(y) == x) rc(x) = y, lc(y) = b;
else lc(x) = y, rc(y) = b;
return ;
}
inline void Splay(const int &x) {
que[qr = 0] = x;
for (int y = x; !isRoot(y); y = fa(y)) que[++qr] = fa(y);
for (int i = qr; i >= 0; --i) downdate(que[i]);
// 由于打了标记,因此根到当前需要Splay 的节点的路径上的标记需要全部下放
while (!isRoot(x)) {
if (!isRoot(fa(x))) {
if (which(x) == which(fa(x))) Rotate(fa(x));
else Rotate(x);
}
Rotate(x);
}
return ;
}
inline void access(int x) {
for (int y = 0; x; y = x, x = fa(x)) {
Splay(x); rc(x) = y;
if (y) fa(y) = x;
}
return ;
}
inline int findRoot(int x) {
access(x); Splay(x);
while (downdate(x), lc(x)) x = lc(x);
Splay(x); return x;
}
inline void makeRoot(const int &x) {
access(x); Splay(x);
rev(x) ^= 1; return ;
}
inline void Link(const int &x, const int &y) {
makeRoot(x); fa(x) = y;
return ;
}
inline void Cut(const int &x, const int &y) {
makeRoot(x); access(y); Splay(y);
lc(y) = 0; fa(x) = 0; return ;
}
char ch;
inline int read() {
while (ch = getchar(), ch < '0' || ch > '9');
int res = ch - 48;
while (ch = getchar(), ch >= '0' && ch <= '9') res = res * 10 + ch - 48;
return res;
}
int main() {
n = read(); m = read();
for (int i = 1; i <= m; ++i) {
while (ch = getchar(), ch < 'A' || ch > 'Z');
if (ch == 'Q') {
if (findRoot(read()) == findRoot(read())) puts("Yes");
else puts("No");
}
else if (ch == 'C') Link(read(), read());
else Cut(read(), read());
}
return 0;
}