介绍
动态树是有关树的结构变化的问题。(将树断边,连边, 改变树的中序遍历)
而 L C T LCT LCT 就是用来解决这类问题的。
L C T LCT LCT 本质上是用 s p l a y splay splay 来维护,所以前置知识就是 s p l a y splay splay。(关于 s p l a y splay splay 可以看看我的另一篇博客)
原理
对于一棵树,我们有重链这个概念,但是和树链剖分的重链不同,这里的重链是可以被改变的。
我们将每一个重链用 s p l a y splay splay 维护,对于 s p l a y splay splay 树之间我们用路径父亲来记录,也就是我们记录当前 s p l a y splay splay 的根的父亲节点,但是父亲节点并不记录你,这些 s p l a y splay splay 树就是辅助数,这些辅助数就构成了森林。
而重链的定义就是你访问过一个节点,那就将它的父亲的重儿子变为它,也就是将他们之间的边变成重链,把原本的重链变成轻链。
我们通过对辅助树的改变,快速对描述树(也就是原树)进行操作。
由于我们是对重链用辅助树维护,则在辅助树中该节点的左子树就是自己的祖先。
我们将所有的辅助树用“路径-父亲”(也就是原树中除了重链的边)连接起来,则辅助树的森林就变为了一棵树。
有些人习惯用标记来判断之间的边是否是“路径-父亲”,但其实如果你们之间的便不是路径父亲,说明你的左右儿子中有一个是我,所以没必要用标记来判断,直接判断我的父亲的儿子中有没有我就可以判断出你和你父亲连的是否是“路径-父亲”。
操作
a c c e s s access access
第一个也是最重要的一个操作 a c c e s s access access 。
这个操作就是将该节点到根的路径全部变为重链。
代码
void access(int x) {
int last = 0;
while (x) {
splay(x);
//现将该节点转到辅助树的根。
tree[x].son[1] = last;
//将两个点之间的边变为重链
last = x;
x = tree[x].fa;
}
}
f i n d find find_ r o o t root root
这个操作就是求这个节点在描述树的根。
知道了 a c c e s s access access 操作,这个操作就变得很简单了。
我们首先先将该节点到根的路径变为重链,由于在辅助树中你的左儿子是你的祖先,则一直往左儿子走,知道走不动了为止,此时的点就是根节点。
代码
int find_root(int x) {
access(x), splay(x);
//现将该节点变为辅助树的根,此时的左儿子才是自己的祖先。
while (tree[x].son[0])
x = tree[x].son[0];
return x;
}
c h a n g e r o o t changeroot changeroot
这个操作就是将这个点变成在描述树中的根。
我们首先 a c c e s s access access ,这样子这个点到目前的根的所经过的点都在同一颗辅助树里了。
我们再 s p l a y splay splay 一下,将这个点变为该辅助树的根。
此时这个点没有右儿子,只有左儿子,我们只需要将该辅助树里的每个节点交换一下左右儿子就行了,因为交换完后这个点到根的路径中就没有父亲了,也就是这个节点成了当前描述树的根。
为了加快速度,我们可以将该操作用懒标记记录,知道访问到这个节点再转。
需要注意的是,如果有了这个操作,在每一次 s p l a y splay splay 前,都要把自己的所有辅助树中的祖先清除一下懒标记。
代码
void pushdown(int x) {
//把懒标记下传
if (tree[x].flag) {
tree[tree[x].son[0]].flag ^= 1, tree[tree[x].son[1]].flag ^= 1;
swap(tree[x].son[0], tree[x].son[1]), tree[x].flag = 0;
}
}
void push(int x) {//这个操作在每次splay前做就可以了
if (pd(x))//判断一下这个节点到根的路径是否是重链
push(tree[x].fa);
pushdown(x);
}
void changeroot(int x) {
access(x), splay(x), tree[x].flag ^= 1;
}
c u t cut cut
前面的操作都没有对树的形态发生改变,而这个操作就是在描述树中断开两个点之间的连边。
我们首先将其中一个点变为其所在的描述树的根,再将另一个节点 a c c e s s access access 一下,此时被 a c c e s s access access 的节点根其的左儿子的路径就是要被断开的路径,我们直接把它们断开就行了。
代码
void cut(int x, int y) {
changeroot(x), access(y), splay(y);
tree[y].son[0] = 0, tree[x].fa = 0;
//断开两个点
}
l i n k link link
有了断边操作就一定有连边操作。
我们首先将一个点变为其所在的描述树中的根。
此时我们只需要将l两个节点中连一条“路径-父亲”就行了。
void link(int x, int y) {
change(y);
tree[y].fa = x;
}
总代码
int pdd(int x) { return tree[tree[x].fa].son[1] == x; }
//判断我是不是我父亲的右儿子
int pd(int x) { return tree[tree[x].fa].son[1] == x || tree[tree[x].fa].son[0] == x; }
//判断我跟我父亲之间的边是不是“路径-父亲”
void rotato(int x) {
int y = tree[x].fa, z = tree[y].fa, p = pdd(x);
if (pd(y))
tree[z].son[pdd(y)] = x;
tree[x].fa = z, tree[y].fa = x;
tree[y].son[p] = tree[x].son[p ^ 1], tree[tree[x].son[p ^ 1]].fa = y;
tree[x].son[p ^ 1] = y;
}
void pushdown(int x) {
if (tree[x].flag) {
tree[tree[x].son[0]].flag ^= 1, tree[tree[x].son[1]].flag ^= 1;
swap(tree[x].son[0], tree[x].son[1]), tree[x].flag = 0;
}
}
void push(int x) {
if (pd(x))
push(tree[x].fa);
pushdown(x);
}
void splay(int x) {
push(x);
for (int y; pd(x); rotato(x)) {
y = tree[x].fa;
if (pd(y))
rotato(pdd(y) == pdd(x) ? y : x);
}
}
//上面三个都是splay的操作
void access(int x) {
int last = 0;
while (x) {
splay(x);
tree[x].son[1] = last;
last = x;
x = tree[x].fa;
}
}
void changeroot(int x) { access(x), splay(x), tree[x].flag ^= 1; }
void split(int x, int y) { changeroot(x), access(y), splay(y); }
//这个操作就是一个混合操作,将一个节点变为其所在的描述树的根,另一个节点access一下并变为其所在的辅助树的根。
//只是因为许多操作都将上面所说的三个操作一起用,则新开了一个函数来表示。
int find_root(int x) {
access(x), splay(x);
while (tree[x].son[0]) x = tree[x].son[0];
return x;
}
void link(int x, int y) {
changeroot(y);
tree[y].fa = x;
}
void cut(int x, int y) {
split(x, y);
tree[y].son[0] = 0, tree[x].fa = 0;
}