在学习动态树之前,你需要能够熟练掌握Splay算法和树链剖分的原理和模板,并知道每一步的细节,如果还比较模糊,可以直接点击超链接,看我的博客(给自己打个广告)。
动态树是指一类问题,通常要求对一片森林进行路径查找与修改、增删边、维护连通性、路径上的权值和等信息等操作,它与树链剖分的区别在于需要维护边的删除和添加。动态树有很多种解决方法,我们今天只介绍适用范围最广、最常用的一种解法:Link-Cut-Tree(LCT)。
(插个眼,等我以后学了其他方法也更在这个博客里吧,到时候再改个名字)
来看下面这条例题:(洛谷P3690 【模板】动态树) LCT同样也是用树链剖分的思想,将森林划分成若干链的形式,为了不与树链剖分混淆,我们用实边、虚边来表示每条边的划分方案(实链剖分):
1、对于某一个节点u,我们可以通过自定义的方式选择一条子边作为实边,其他边作为虚边
2、一个节点最多有一条与孩子相连的实边,可以没有实边。
3、相邻实边连接作为一条实链
4、没有孩子的叶节点也作为一条实链
5、实链中深度最浅的点被称为顶点
6、顶点与其父节点连接的边必定是虚边
7、实边在操作过程中是可变的,当需要将实边变化时,我们只需要更改交换两个改变的孩子即可
实链剖分的强大在于链的划分是可变的,动态的,对于链上求权值和我们需要找一个同样灵活可变的数据结构来维护,那必然是我Splay了! 维护方案:
1、每一条链由一棵splay维护
2、splay的中序遍历即为实链的上下顺序
3、每个节点在splay中的前驱是它的父节点,后继则是它的子节点
4、无论splay如何旋转,都不会影响实链的先后顺序
利用原树中的虚边,我们可以将splay们连成一棵树:
新树的每个节点都是一棵splay维护的实链,由于原本的splay的根结点没有父亲节点,那么我们就可以把虚边的关系存在splay的根结点中,注意:对于每条虚边,我们只存储了父节点在splay的根结点中,并没有存子节点的关系,也就是只认父亲不认孩子
新树的性质:
1、每个节点都是一棵splay树,对应原树中的实链
2、所有的虚边关系由splay的根结点保存
3、新树只保存了不同实链之间的关系,故可以在满足性质的情况下随意换根
4、虚实边的变化在新树上可以很轻松的维护,所以我们所有的操作都在新树上完成即可
LCT的操作函数:
1、access(int x):在原树中建立一条根结点到x的实链
2、update(int x):把x的标记下传到整个splay树
3、make_root(int x):将x变为原树中的根结点
4、find_root(int x):找到x所在原树的根节点, 再将原树的根节点旋转到splay的根节点
5、split(int x, int y):将x到y的路径变为实链
6、link(int x, int y):如果x与y不连通,加入新边x,y
7、cut(intx, int y):删除x与y连接的边
8、isRoot(int x):判断x是否为splay的根结点
我们从代码角度介绍一下每个函数的目的和实现方法 :
1、access(int x):在原树中建立一条根结点到x的实链
我们可以先把x转到splay的根结点,那么它的父节点y和它的边一定为虚边,将这条边变成实边也就是将x更新为y的后继,于是我们再把y旋转到y所在splay的根结点,由于y在原链中是最低的点,也就是说y原本是没有后继的,所以y旋转到根结点后它的右子树一定是空的,此时我们将x所在splay插在y的右子树即可。依次从下往上操作直到根结点位置。
这个操作是LCT的核心操作,后续的很多操作都是以该函数为基础的。
void access(int x) { //建立一条从根到x的路径,同时将x变成splay的根节
int y = 0,u = x;
while(u) {
splay(u);
tr[u].s[1] = y, pushup(u);
y = u; u = tr[u].p;
}
splay(x);
}
2、update(int x):把x的标记下传到整个splay树
3、make_root(int x):将x变为原树中的根结点
为了将x变为原树中的根结点,我们可以先access一遍x,此时x作为实链的最后,根结点作为开头,只要翻转一下链即可。
void pushrev(int x) { // 翻转x为顶点的实链
swap(tr[x].s[0], tr[x].s[1]);
tr[x].rev ^= 1;
}
void makeroot(int x) { // 将x变成原树的根节点
access(x);
pushrev(x);
}
4、find_root(int x):// 找到x所在原树的根节点, 再将根节点旋转到splay的根节点
首先用一遍access将x与根结点放在同一条实链上,由于根结点在原来的链上是顶点,所以只需要找x作为splay的根结点的最左边的节点即可。记得更新自下而上地更新标记。
int findroot(int x) { // 找到x所在原树的根节点, 再将原树的根节点旋转到splay的根节点
access(x);
while (tr[x].s[0]) pushdown(x), x = tr[x].s[0];
splay(x);
return x;
}
5、split(int x, int y):将x到y的路径变为实链
有了函数4实现这一步就特别简单,直接将x移到根,然后access一遍即可。
void split(int x, int y) { // 给x和y之间的路径建立一个splay,其根节点是y
makeroot(x);
access(y);
}
6、link(int x, int y):如果x与y不连通,加入新边x,y
主要是判断x与y是否在同一条链上。可以先将x移到原树的根,再findroot一下y的根是否与x相等即可。
void link(int x, int y) { // 如果x和y不连通,则加入一条x和y之间的边
makeroot(x);
if (findroot(y) != x) tr[x].p = y;
}
7、cut(intx, int y):删除x与y连接的边
删除操作与连接操作类似,只需要把父子关系指针置0即可。
void cut(int x, int y) { // 如果x和y之间存在边,则删除该边
makeroot(x);
if (findroot(y) == x && tr[y].p == x && !tr[y].s[0])
{
tr[x].s[1] = tr[y].p = 0;
pushup(x);
}
}
8、isRoot(int x):判断x是否为splay的根结点
根据我们存储虚边的特性,儿子是知道父亲是谁,但是父亲不知道儿子,那么我们就判断x的父亲的两个儿子是否是x即可。
bool isroot(int x) {
return tr[tr[x].p].s[0] != x && tr[tr[x].p].s[1] != x;
}