Link-Cut Tree
实链剖分
就是可以随意乱剖,想干嘛干嘛的剖分。我们面对动态树问题(树形会变化的问题),用实链剖分可以灵活的维护。然后因为实链剖分比较灵活,我们类比重链剖分,也考虑使用一种数据结构维护这个树上信息。论灵活度,相比线段树来说,splay
是更好的选择。
LCT
也叫 link-cut tree
,用于维护动态森林。具体来说可以被称为一种辅助森林,里面有很多辅助树,每个辅助树由很多 splay
树组成,而每个 splay
维护树上的一条实链。一般来说比较擅长维护路径信息,处理子树的信息似乎不太行?
而LCT有以下的一些性质:
1.具体考虑一下 splay 怎么把实链分开?实际上就是对于有虚边相连的两条实链,它们的 splay 的关系是 儿子认父亲,父亲不认儿子
。那么显然,每个节点最多向其中一个儿子拉一条实链(暂且不考虑一个 splay 内部发生 splay 操作的情况下,也在就是原树中)。
2.每个点只包含在 LCT 中的一个 splay 中。
3.LCT 中的 splay 保证中序遍历下深度严格递增。因为每条实链都是按照原树剖分得到的结果。因此深度相同的点也不应当出现在同一棵 splay 中。
具体操作
splay基操
由于我们 LCT 的性质1,我们判断一个点是否存在于当前 splay需要换一下,除此之外没有任何区别。
我们判断一个点是不是当前 splay 的根的方法并非看它是否拥有父节点,而是看其父节点是否认它。那么我们判断一个点是否存在于当前 splay 就是看它的儿子是否为当前 splay 的根;若为根,则不为 splay 中节点,反而反之。
别的都没什么区别,不多说了,直接上代码(可以看看一些细节变化):
inline void change(int x){//按修改
t[x].rev^=1;
std::swap(lc(x),rc(x) );
}
inline void pushdown(int x){//标记下传
if(t[x].rev){
change(lc(x) );
change(rc(x) );
t[x].rev^=1;
}
}
inline void pushup(int x){//向上维护信息
t[x].sz=(t[lc(x)].sz+t[rc(x)].sz)+1;
}
inline bool Get(int x){//判断所属儿子类型
return x==rc(fa(x) );
}
inline bool isroot(int x){//判断是否为根
return lc(fa(x) )!=x and rc(fa(x) )!=x;
}
inline void rotate(int x){
int y=fa(x),z=fa(y);bool ty=Get(x);
if(!isroot(y) ) t[z].ch[Get(y)]=x;
//由于isroot的判定方法,所以提前
t[y].ch[ty]=t[x].ch[ty^1];
if(t[x].ch[ty^1]) fa(t[x].ch[ty^1])=y;
t[x].ch[ty^1]=y;
fa(y)=x;fa(x)=z;
pushup(y);pushup(x);
}
void update(int x){//提前下传tag
if(!isroot(x) ) update(fa(x) );
pushdown(x);
}
inline void splay(int x){
update(x);//先下传好标记,不然旋转的时候下传显然是错的
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x) ){
if(!isroot(f) ) rotate(Get(x)==Get(f)?f:x);
}
}
比较需要注意的是我们的 change 函数不应该根据当前点是否具有翻转标记来决定,因为我们是一传了标记就给它换了,所以之后如果再次打了翻转标记应当换回来。
access(连通)
核心函数之一。
我们使用这个操作,使得整个辅助树内的根与当前点 \(x\) 直接开出一条实链(我们形象地称之为 ✟升天✟)
具体操作如何?我们先从 \(x\) 点开始,使之升至当前 splay 的根节点,并且断开一条连向右儿子的边。然后我们取到其父节点以跳到上方的 splay,然后断右儿子,连向我们刚刚跳过来的地方。如此反复直到到达根节点。
为什么要这样搞呢?因为当我们要新连出一条实边,我们必然需要在原树上断开一条原先的实边。而断开右儿子的原因便是因为右儿子的深度由于性质3一定更大,而我们显然不应该断开一条连向原树中父亲的边来换一条实边,否则这直接就不是树了。因此我们断开右儿子来为下方的连通腾出位置。
inline int access(int x){
int p;
for(p=0;x;p=x,x=fa(x) ){
splay(x);rc(x)=p;pushup(x);
}
return p;
}
你发现我这个函数有返回值,这个返回值是我们连通的过程中由虚变实的最后一个连接点,也就是最后一条虚变实的边中更高的那一个点。我们看看这东西有没有什么性质。
我们发现在做完一次 access 的情况下,我们再做一次,那么这个返回值是两点的 LCA。因为在我们第一次已经打通了一条实链的情况下,其他点要去开根一点会要通过一条虚边进入这条实链,而且经过之后不会再经过虚边,所以可以得到 LCA。所以这个可以用来求 LCA。
而且显然,在一个点做完 access 之后,就变成整个 splay 中中序遍历最后遍历的点。因为我们第一次就直接把它的右儿子断开了。
makeroot(换根)
核心函数之二。
发现如果我们需要维护一条深度不单调递增的路径,由于性质3这条路径的点不可能出现在同一棵 splay 下。
那我们怎么办!要寄了吗?.jpg
因此这个函数存在的意义可知矣。这个操作能将一个节点换为当前的根。发现只需要连通 \(x\) 与根,并 splay \(x\) 即可。但是我们发现还有一点问题,这个时候的 \(x\) 仅仅是名义上坐在根的位置上,但是并没有以他为根的性质。
我们发现如果我们把树看成从父亲向儿子的有向图,在原树上我们需要将根到 \(x\) 路径上的边方向反向。这启发了我们,当我们做完 access 后,这个点变为中序遍历最后遍历的点。我们要使其变为根,就是使其变为中序遍历最先遍历的点,那么我们直接给它打上翻转标记,由于其没有右子树,只有左子树,我们翻转过后就使其正好成为中序遍历最先遍历的点了,达到目的。
inline void makeroot(int x){
access(x);splay(x);
change(x);
}
find(寻根)
找到 \(x\) 所在辅助树的根。
直接 access(x) 然后 splay(x) 就可以得到 splay 树最高的位置,然后一直跳左儿子找到深度最低的点即为答案。
别忘了找跳左儿子的时候传标记已经最后将根旋到 splay 的根以保证复杂度。
inline int find(int x){
access(x);splay(x);
while(lc(x) ){
pushdown(x);
x=lc(x);
}
splay(x);
return x;
}
link(连接)
直接拿出这个条边端点的一个,把它 makeroot 然后直接把它父亲接到另一个即可。
有的时候需要检验操作是否合法,用 find 即可。
inline void link(int x,int y){
makeroot(x);
if(find(y)==x) return;
fa(x)=y;
}
split(抽离)
抽出维护路径 \(x\) 到 \(y\) 的splay。
把 \(x\) makeroot,然后 access \(y\) 即可。
inline void split(int x,int y){
makeroot(x);access(y);splay(y);
}
把 \(y\) 提起来之后直接访问 \(y\) 就可以访问路径信息了。
注意最后这个 splay 不可省去,因为 \(y\) 还在老底下没上来, \(x\) 也被 access 给创下去了。
cut(切割)
如果合法的话操作非常简单,我们直接 split,此时 \(x\) 为原树的根,\(y\) 为辅助树的根,那么 \(x\) 在辅助树上肯定为 \(y\) 的左儿子。直接断开。
inline void cut(int x,int y){
split(x,y);fa(x)=lc(y)=0;
pushup(y);
}
否则我们考虑怎么判断不合法。
我们首先把 \(x\) makeroot,然后用 find 判断连通性,此时 \(x\) 被拉至根,我们要判断他们直接是否有连边,只需要满足 \(x\) 为 \(y\) 的父亲,并且 \(y\) 没有左子树(这样可知 \(x\) 与 \(y\) 深度差 \(1\) )。最后更新一下信息就好。
inline void cut(int x,int y){
makeroot(x);
if(find(y)!=x or fa(y)!=x or lc(y) ) return;
fa(y)=rc(x)=0;
pushup(x);
}
其实也可以:
inline void cut(int x,int y){
makeroot(x);
if(find(y)!=x or t[x].sz>2) return;
fa(y)=rc(x)=0;
pushup(x);
}
因为如果有边相连,它们 access(y) 之后肯定只有这两个点了。
主要操作就这些了。
(那差不多就没啦?)
参考代码
#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
inline bool blank(const char &c){
return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
}
inline void gs(char *s){
char ch=gc();
while(blank(ch) ) {ch=gc();}
while(!blank(ch) ) {*s++=ch;ch=gc();}
*s=0;
}
inline void gs(std::string &s){
char ch=gc();s+='#';
while(blank(ch) ) {ch=gc();}
while(!blank(ch) ) {s+=ch;ch=gc();}
}
inline void ps(char *s){
while(*s!=0) pc(*s++);
}
inline void ps(const std::string &s){
for(auto it:s)
if(it!='#') pc(it);
}
template<class T>
inline void read(T &s){
s=0;char ch=gc();bool f=0;
while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
if(ch=='.'){
db p=0.1;ch=gc();
while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
}
s=f?-s:s;
}
template<class T,class ...A>
inline void read(T &s,A &...a){
read(s);read(a...);
}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e5+3;
struct LCT{
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
#define fa(x) t[x].fa
struct node{
int fa,ch[2],val;
int sz,sum;
bool rev;
}t[N];
inline void change(int x){
t[x].rev^=1;
std::swap(lc(x),rc(x) );
}
inline void pushdown(int x){
if(t[x].rev){
change(lc(x) );
change(rc(x) );
t[x].rev=0;
}
}
inline void pushup(int x){
t[x].sz=(t[lc(x)].sz+t[rc(x)].sz)+1;
t[x].sum=(t[lc(x)].sum^t[rc(x)].sum)^t[x].val;
}
inline bool Get(int x){
return x==rc(fa(x) );
}
inline bool isroot(int x){
return lc(fa(x) )!=x and rc(fa(x) )!=x;
}
inline void rotate(int x){
int y=fa(x),z=fa(y);bool ty=Get(x);
if(!isroot(y) ) t[z].ch[Get(y)]=x;
//由于isroot的判定方法,所以提前
t[y].ch[ty]=t[x].ch[ty^1];
if(t[x].ch[ty^1]) fa(t[x].ch[ty^1])=y;
t[x].ch[ty^1]=y;
fa(y)=x;fa(x)=z;
pushup(y);pushup(x);
}
void update(int x){
if(!isroot(x) ) update(fa(x) );
pushdown(x);
}
inline void splay(int x){
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x) ){
if(!isroot(f) ) rotate(Get(x)==Get(f)?f:x);
}
}
inline int access(int x){
int p;
for(p=0;x;p=x,x=fa(x) ){
splay(x);rc(x)=p;pushup(x);
}
return p;
}
inline void makeroot(int x){
access(x);splay(x);
change(x);
}
inline int find(int x){
access(x);splay(x);
while(lc(x) ){
pushdown(x);
x=lc(x);
}
splay(x);
return x;
}
inline void link(int x,int y){
makeroot(x);
if(find(y)==x) return;
fa(x)=y;
}
inline void split(int x,int y){
makeroot(x);access(y);splay(y);
}
inline void cut(int x,int y){
makeroot(x);
if(find(y)!=x or t[x].sz>2) return;
fa(y)=rc(x)=0;
pushup(x);
}
#undef lc
#undef rc
#undef fa
}s;
int main(){
filein(a);fileot(a);
int n,m;
read(n,m);
for(int i=1;i<=n;++i){
read(s.t[i].val);
}
for(int i=1;i<=m;++i){
int op;read(op);
if(op==0){
int x,y;
read(x,y);
s.split(x,y);
printf("%d\n",s.t[y].sum);
}else if(op==1){
int x,y;
read(x,y);
s.link(x,y);
}else if(op==2){
int x,y;
read(x,y);
s.cut(x,y);
}else{
int x,y;
read(x,y);
s.splay(x);
s.t[x].val=y;
s.pushup(x);
}
}
return 0;
}