动态树学习总结
动态树的介绍
动态树,简称
百度上对动态树给出的定义如下:
动态树问题, 即要求我们维护一个由若干棵子结点无序的有根树组成的森林,支持对树的分割, 合并, 对某个点到它的根的路径的某些操作, 以及对某个点的子树进行的某些操作。
动态树擅长的问题是动态维护树的形态(名为动态树的原因所在)、对森林进行维护、在树上进行路径的修改和维护,处理这些的复杂度为均摊
(可惜
当然动态树还可以做的事情有很多,我们先从简单的开始入手,一步步深入
注:学习
动态树的实现与基本操作
虚边与实边
类似于树链剖分用轻链和重链维护树的形态,动态树是通过虚边和实边对树的形态进行存储的,一个节点至多有1条连接儿子的实边,如图:
(这张图中
图中红色的实线表示实边,蓝色的虚线表示虚边。
那在程序中实边与虚边怎么区分呢?
实边:儿子能直接找到通过实边连接的父亲,且父亲也能直接找到通过实边连接的儿子。
虚边:儿子能直接找到通过虚边连接的父亲,但父亲不能直接找到通过虚边连接的儿子(认父不认子)。
我们用
树链剖分的轻重链是根据子树的大小确定的,它是固定的。而动态树的虚实边是可以任意任命的,它是不固定的。因此,我们可以改变一条边的虚实。
比如说,我们把
通过思考和图片,我们可以发现,我们只需要把
因此,当我们要将一条边改为实边时,只需要将父节点的儿子改为这条边连接的子节点。
链上信息维护
类似于树链剖分,在划分完虚边与实边后,整棵树被分成了一条条由实边连接而成的链(即实链)。
为了快速维护实链上的信息,我们使用
如图,
先把刚才的图的虚实边修改一下成为这个样子:
然后我们拿出
(对于
我们会发现,
如果我们像上图一样维护
原树中链的顶端——
那我们该怎么办呢?
统观整棵
所以,我们在
修改后,这棵
在
答案是不用的。我们可以通过一个方法判断出一个点是否是Splay的根节点。
(每个绿色框内为一棵
由于Splay的根节点的父亲存的是虚边的父节点,根据之前提到的虚边认父不认子,Splay的根节点的父亲的任何一个子节点都不会是这个根节点。
例如,
再例如,
其他的根节点同理。
因此,判断一个点是否是
基本操作
存储\textbf{Splay} 的结构体(不知道该放哪就放在这了)
struct node{
int s[2];//s[0]左儿子,s[1]右儿子
int p;//父亲
int rev;//翻转标记
//这三个是必须有的,用于维护Splay的形态(rev标记在一般的LCT中都需要)。
//其他需要维护的信息具体问题具体分析。
//常见的有v(节点的权值),sum(自己和子树的权值和),sz(自己和子树的节点个数)等。
}tr[N];
\textbf{push\_up(x)} ,\textbf{push\_down(x)}
作用:上传更新信息和下传标记,具体问题具体分析。
\textbf{push\_rev(x)}
作用:将
实现:思想与
代码:
void push_rev(int x){
swap(tr[x].s[0],tr[x].s[1]);
tr[x].rev^=1;
}
\textbf{isroot(x})
作用:判断
实现:通过判断
代码:
bool isroot(int x){
return tr[tr[x].p].s[0]!=x&&tr[tr[x].p].s[1]!=x;
}
\textbf{rotate(x)}
作用:进行一次翻转。
实现:基本同
代码:
void rotate(int x){
int y=tr[x].p,z=tr[y].p,k=tr[y].s[1]==x;
if(!isroot(y)) tr[z].s[tr[z].s[1]==y]=x;//在这里特判
tr[x].p=z;
tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
tr[x].s[k^1]=y,tr[y].p=x;
push_up(y),push_up(x);//旋转后要自下而上更新一下信息(此时y在x的下方)
}
\textbf{push\_all(x)}
作用:将
实现:从
代码:
void push_all(int x){
if(!isroot(x)) push_all(tr[x].p);
push_down(x);
}
\textbf{splay(x)}
作用:将
实现:先将标记全部下传,再进行旋转。旋转同
代码:
void splay(int x){
push_all(x);
for(;!isroot(x);rotate(x)){
int y=tr[x].p,z=tr[y].p;
if(!isroot(y)) rotate((tr[z].s[1]==y)^(tr[y].s[1]==x)?x:y);
}
}
\textbf{access(x)} (\textbf{LCT} 的核心操作)
作用:打通一条从
实现:
我们拿这张图来举例子。
我们来
此时原树的根节点为
我们考虑从
首先,我们要保证这条路径是不包含其他点的,所以我们将原树中
对应的,我们需要在
修改这一步后变成了这样:
下一步,我们将原树中
对应在
现在是这个样子:
最后,我们以相同的方法,将
这样,我们便打通了从
边界判定为当前处理的节点是否为
(这个例子中没有包含打通路径时某个父节点所在
代码:
void access(int x){
for(int y=0;x;y=x,x=tr[x].p){//y=0的原因是开始时右儿子应设为空,之后y为上一次操作后的根节点。
splay(x);//将x转到根
tr[x].s[1]=y;//上次操作的根为现在点的右儿子
push_up(x);//更新信息
}
}
\textbf{makeroot(x)}
作用:把
实现:
打通
这样操作的原因:
打通
然后我们将整棵
这样,
整棵
如果语言解释不能使您明白的话,那就上图!
我们将
首先
然后
然后
(在程序实际运行时,
这样就完成了原树的换根。
代码:
void makeroot(int x){
access(x);
splay(x);
push_rev(x);
}
\textbf{findroot(x)}
作用:找到
实现:
首先打通
从
找左儿子时别忘了下传标记!
找完记得把原树的根转到
代码:
int findroot(int x){
access(x);
splay(x);
while(tr[x].s[0]) push_down(x),x=tr[x].s[0];
splay(x);
return x;
}
\textbf{split(x,y)}
作用:打通一条从
实现:
把
注:如果
代码:
void split(int x,int y){
makeroot(x);
access(y);
splay(x);//这里也可以写splay(y),这样Splay的根为y
}
\textbf{link(x,y)}
作用:加一条连接
实现:
先把
如果题目不保证
将
代码:
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x)//进行连通性判断
tr[x].p=y;
}
\textbf{cut(x,y)}
作用:断掉直接连接
实现:
打通
如果题目不保证
动态树的实战应用
1.链上修改查询
这类问题是使用
下面是例题:
P3690 【模板】动态树(Link Cut Tree)