LCT 初步
LCT(Link Cut Tree)是一种动态维护树的结构变化以及相关信息的数据结构。
对 LCT 的理解
LCT 本质上是用 Splay 维护树链剖分:
- Splay 指伸展树,是一种自适应的平衡树;
- 树链剖分则是指广义地将树剖分为若干个链,而非狭义的轻重链剖分。
总体来说,LCT 可以解决以下的问题:
- 连接两个点;
- 删除某条边;
- 维护可合并的路径信息。
具体来说,Splay 按照树链从深到浅的顺序维护树链的结点。而 LCT 就是一个 Splay 森林。
在实现过程中,我们一般使用父节点和子节点指针维护 Splay 的结果。
如果一对父子结点的指针是相互的,那么连接这两个点的边属于 Splay 上的边。
否则会出现父节点的子节点指针没有指向自己的情况,那么这条边就属于原树上的边。
简单地说,LCT 的父节点指针在 Splay 内就是 Splay 的父节点指针,在 Splay 外(根节点的父节点指针)表示的就是这条树链的链顶的父节点指针。
LCT 的实现
接下来我们介绍一个函数式的 LCT 的代码实现。
通用函数
为了方便新节点的定义,我们封装一个函数:
int new_node(int v);
然后是老生常谈的信息合并与标记下传函数:
void pushup(int u);
void pushdown(int u);
为了支持原树根节点的变动,我们需要支持给 Splay 打翻转标记,表示这条树链的父子关系翻转。因此需要有一个打标记的函数:
void noderv(int u);
核心函数
接下来是有关 LCT 结构维护的核心函数。
为了代码的可读性,我们封装一个函数用于判断该结点是否是所在 Splay 的根节点:
bool isroot(int u);
同时封装一个函数用于获取这个结点是 Splay 上的左儿子还是右儿子:
bool get(int u);
接下来就是 Splay 的核心函数——旋转和伸展。但需要做一些小改动,比如判断当前结点是否是当前 Splay 的根:
void rotate(int u);
void splay(int u);
接下来是 LCT 的核心函数——贯通。它的作用是将结点到原树的根节点的路径变成一条树链,其他受影响的链会被截断一部分。
void access(int u);
然后是维护原树根节点变化的函数,它的作用是将结点变成原树的根节点:
void makeroot(int u);
常用函数
接下来是一些常用的函数,基于核心函数。但不是必需。
bool check_link(int x,int y);
void link(int x,int y);
bool check_edge(int x,int y);
void cut(int x,int y);
- 检查是否连通;
- 在不连通的之间连一条边;
- 检查的边是否存在;
- 删除这条边(保证存在)
完整代码实现
/******************heading******************/
const int SZ=1e6+6;
int tot,ch[SZ][2],fa[SZ],val[SZ],sz[SZ],rv[SZ];
// 通用函数
int new_node(int v){
++tot,ch[tot][0]=ch[tot][1]=fa[tot]=rv[tot]=0;
sz[tot]=1,val[tot]=v,sxor[tot]=v;
return tot;
}
void pushup(int u){
sz[u]=sz[ch[u][0]]+sz[ch[u][1]]+1;
}
void noderv(int u){ if(u)rv[u]^=1; }
void pushdown(int u){
if(rv[u])swap(ch[u][0],ch[u][1]),noderv(ch[u][0]),noderv(ch[u][1]),rv[u]=0;
}
// 核心函数
bool isroot(int u){return ch[fa[u]][0]!=u&&ch[fa[u]][1]!=u;}
bool get(int u){return ch[fa[u]][1]==u;}
void rotate(int u){
int p=fa[u],pp=fa[p],k;
pushdown(p),pushdown(u),k=get(u);//k 的赋值必须在 pushdown 后!
if(!isroot(p))ch[pp][get(p)]=u;//!!!
ch[p][k]=ch[u][!k], fa[ch[u][!k]]=p;
ch[u][!k]=p,fa[p]=u, fa[u]=pp;
pushup(p),pushup(u);
}
void splay(int u){
pushdown(u);
for(int p;p=fa[u],!isroot(u);rotate(u))
if(!isroot(p))rotate(get(p)==get(u)?p:u);
}
void access(int u){
for(int p=0;u;p=u,u=fa[u])splay(u),ch[u][1]=p,pushup(u);
}
void makeroot(int u){ access(u), splay(u), noderv(u); }
// 常用函数
bool check_link(int x,int y){
makeroot(x),access(y),splay(x),splay(y);
return !(isroot(x)&&isroot(y));
}
void link(int x,int y){ makeroot(x), fa[x]=y; }
bool check_edge(int x,int y){
if(!check_link(x,y))return 0;
makeroot(x),access(y),splay(y);
if(ch[y][0]!=x||ch[x][1])return 0;
return 1;
}
void cut(int x,int y){
makeroot(x),access(y),splay(y),ch[y][0]=fa[x]=0,pushup(y);
}
// 题目相关函数
void nodeassign(int u,int v){ val[u]=v, pushup(u); }
void assign(int x,int y){ splay(x), nodeassign(x,y); }
int query(int x,int y){ return makeroot(x), access(y),splay(y),sxor[y]; }
void print(){
puts("---------------------------------");
FOR(u,1,5)printf("u=%2d,lc=%2d,rc=%2d,sz=%2d,f=%2d,rv=%2d\n",
u,ch[u][0],ch[u][1],sz[u],fa[u],rv[u]);
puts("---------------------------------");
}
/*
* 模板:Luogu3690
* new_node: 新建权值为 v 的结点
* pushup: 信息更新
* pushdown: 标记下传,主要是翻转标记
* noderv: 对某一个结点施加标记。
* LCT 的标记不同于线段树,必须在下传的时候再更新当前结点的信息。不然
* get 的时候会出锅
* nodeassign: 模板题需要
* isroot: 是否是所在 Splay 的根
* get: 是 Splay 上左儿子还是右儿子
* print: 调试函数
* rotate: 双旋,注意与 Splay 的双旋不同,要判 fa[u] 是不是 root,不然 fa[fa[u]] 的
* 儿子不能乱赋值
* splay: 把当前结点旋转到当前 Splay 的根结点,要用到 isroot 函数。一开始
* 先 pushdown。
* access: 把当前结点到根的路径连成一个 Splay,注意这个 Splay 只包含当前结点
* 到根这段路径上的点,不包括当前结点子树的那一段(非到叶结点的树链)
* access 完之后这个点不一定是所在 splay 的根,需要手动 splay 一下
* makeroot: 把当前结点变成原树的根,这个结点也会顺便变成所在 Splay 的根。
* check_link: 判断两个点是否连通。
* link: 连接两个不连通的点
* check_edge: 判断两个点是否直连通(有没有边)
* cut: 删掉 (x,y) 的边。
* assign: 模板题需要
* query: 模板题需要
*/
BZOJ 2631
维护 Link/Cut,链加 / 乘 / 求和
对于 Tag 的维护,除了翻转 Tag 是在修改的时候打标记,下传的时候更新当前结点状态;其他标记都是在修改的时候打标记顺便更新该结点信息,在下传的时候更新子节点信息(与线段树相同)
维护虚儿子信息
链上信息可以 Splay 维护。对于虚儿子的信息,我们可以使用 set 维护。那么就只需要在改变实儿子的时候更新这部分信息。也就是在 access,link 和 cut 的时候。
修订记录
- 2021年6月22日 第2次修订
- 2020年5月18日 创建文章