LCT详解(2022修)
今天学了个叫 “LCT” 的东西,很多人估计越听越懵。
Link-Cut Tree 确实是个很不好理解的东西,就连我也是。
就算是老师讲也感觉很模糊。
首先,这个 LCT 也可以理解为“用 Splay 来维护树剖”,这里的树剖指的是实链剖分(专门为 LCT 服务的剖分方式)。
一棵树中,处于同一条实链的节点用同一个 Splay 维护。由于 Splay 灵活多变,完全可以代替线段树。
然后,每一个 Splay 内维护的节点键值是他们在原来树中的深度。即对于一个 Splay 节点键值 \(x\),左子树的所有节点在树中的深度都小于 \(x\),右子树的所有节点在树中的深度都大于 \(x\)。
于是,代码的前半部分完全用来写 Splay。
注意,有一个重点就是,树中的实链(即一棵 Splay)中的节点连的是双向边,而非实边连的是单向边(儿子认父,父不认儿子),这样有助于判断一个节点是否是 Splay 的根。
但是,之后呢?我们定义一个函数 \(access(x)\) 表示,现在把 \(root,...,x\) 这一条链搞成一条实链,并且 \(x\) 下面不接任何实边(注意 \(root,...,x\) 这些节点原本接的实边要删除)
如上图,红色的为实边(有些节点也可以不接实边,比如 \(7\))。
接下来执行 \(access(5)\)。
执行 \(access(8)\)。
可以发现,每做一次 \(access(x)\),其实就是把 \(root,...,x\) 重构成一条实链,并单独用一个 Splay。
\(access\) 函数是 LCT 中最重要的部分,也是 LCT 中唯一连接实线的方式。
void access(int x) //x 不为空,直到根为止
{
int y=0;
while(x)
{
splay(x);
a[x].c[1]=y; //节点 x 原来接的实线清空,重新接到下面的节点,原来接的节点必然深度更深,即在平衡树右边
a[y].fa=x;
pushup(x);
y=x;
x=a[x].fa; //下一次更新实线用到
}
}
函数 \(evert(x)\) :把 \(x\) 作为 \(x\) 所在树的与根。
考虑用 \(access(x)\) 把 \(x\) 和 \(root\) 连接,然后伸展到平衡树的根,最后利用翻转来保证所在平衡树的深度。
比如执行 \(evert(8)\),连接了 \(1,7,8\),以深度为键值建出平衡树:
把 \(8\) 节点伸展到根:
你会发现,如果把 \(8\) 作为根,有且只有这棵平衡树的深度需要重构,只需要对整棵树进行翻转即可。
翻转必然要打标记,必然要下放标记。因此在每次 Splay 伸展时,必须先从根传标记。
void evert(int x)
{
access(x);
splay(x);
a[x].rev^=1; //翻转标记
}
函数 \(findrt(x)\) 查找 \(x\) 所在的树的根。
显然,先用 \(access\) 连接 \(root\) 和 \(x\),然后在平衡树中找深度最小的,即一直往左走。
int findrt(int x)
{
access(x); //连接 root,x
splay(x); //把 x 伸展到平衡树的根,也可以理解为找平衡树的根的一种方式
while(a[x].c[0]) //一直往左走
{
x=a[x].c[0];
}
return x;
}
函数 \(link(x,y)\),连接原来树上 \((x,y)\) 两个节点。
类似于并查集的合并,我们可以先把 \(x\) 作为树根 \(evert(x)\)。
然后把根的父亲接到 \(y\) 后面。注意此时 \(x,y\) 不在同一个 Splay。
void link(int x,int y)
{
if(findrt(x)==
findrt(y))return; //判断根是否相同
evert(x);
a[x].fa=y;
}
函数 \(cut(x,y)\) 表示切断原来书上 \((x,y)\) 两个节点。
类似于 Splay 的分裂,我们就先把 \(y\) 作为原来树根 \(evert(y)\),然后让 \(x\) 打通到根的路径 \(access(x)\)(即使 \((x,y)\) 只有一条边),那么这时 \(y\) 的深度为 \(1\),\(x\) 的深度为 \(2\)。如果 \(x,y\) 之间没有边,那么 Splay 树中 \(x\) 的左孩子就不是 \(y\),或者 \(y\) 有右子树,这样就能判断 \(x,y\) 的深度是否相邻。
然后直接在 Splay 中断掉 \((x,y)\) 边即可。
void cut(int x,int y) //删边
{
evert(y); //把 v 换成根时,已经用 splay 转成了平衡树的根
access(x);
splay(x); //再 splay 一次,x,y在平衡树中就相邻了
if(a[x].c[0]==y&&!a[y].c[1])
{
a[y].fa=0;
a[x].c[0]=0;
pushup(x);
}
}
剩下的 \(query(x,y)\) 也很简单,求路径 \((x,y)\) 的一些信息,比如异或和。
把 \(y\) 作为根,让 \(x\) 打通到 \(y\) 的路径,并且让 \(x\) 伸展到这条实链的 Splay 的根,求一下平衡树的 \(x\) 子树和(即这棵平衡树的和)即可
代码1:LCT基础(P3690【模板】动态树)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000010;
int n,m,x,y,op;
struct node
{
int c[2],sum,fa,rev,v;
}a[maxn];
bool isroot(int i)
{
return !a[i].fa||(a[a[i].fa].c[0]!=i&&a[a[i].fa].c[1]!=i);
}
void pushup(int x)
{
a[x].sum=a[a[x].c[0]].sum^a[a[x].c[1]].sum^a[x].v;
}
void pushdown(int x)
{
if(!a[x].rev)return;
swap(a[x].c[0],a[x].c[1]);
a[a[x].c[0]].rev^=1;
a[a[x].c[1]].rev^=1;
a[x].rev=0;
}
int rotate(int x)
{
int y=a[x].fa,z=a[y].fa;
int kind=(x==a[y].c[0]);
if(isroot(y))
{
a[x].fa=a[y].fa;
}
else
{
if(a[z].c[0]==y) a[z].c[0]=x;
else a[z].c[1]=x;
}
a[y].c[kind^1]=a[x].c[kind];
a[a[x].c[kind]].fa=y;
a[x].c[kind]=y;
a[y].fa=x;
a[x].fa=z;
pushup(y); pushup(x);
}
void renew(int x) //在 splay 伸展之前,记得下传翻转标记,因为伸展一下会改变形态
{
if(!isroot(x))renew(a[x].fa);
pushdown(x);
}
void splay(int x)
{
renew(x); //传一下标记
while(!isroot(x))
{
int y=a[x].fa,z=a[y].fa;
if(!isroot(y))
{
if((a[z].c[0]==y)^(a[y].c[0]==x)) rotate(x);
else rotate(y);
}
rotate(x);
}
}
void access(int x) //打通 root...x 路径,即连出实链
{
int y=0;
while(x)
{
splay(x);
a[x].c[1]=y; //把现在 x 连的实边代替掉之前连的实边
a[y].fa=x;
y=x;
pushup(x); //记得每次构造 Splay 都要更新
x=a[x].fa;
}
}
void evert(int x)
{
access(x);
splay(x);
a[x].rev^=1;
}
int findrt(int x)
{
access(x);
splay(x);
while(a[x].c[0])
{
x=a[x].c[0];
}
return x;
}
void link(int x,int y) //连边
{
if(findrt(x)==findrt(y))return;
evert(x);
a[x].fa=y;
}
void cut(int x,int y) //删边
{
evert(y); //把 v 换成根时,已经用 splay 转成了平衡树的根
access(x);
splay(x); //再 splay 一次,x,y在平衡树中就相邻了
if(a[x].c[0]==y&&!a[y].c[1])
{
a[y].fa=0;
a[x].c[0]=0;
pushup(x);
}
}
int ask(int x,int y)
{
evert(y);
if(findrt(x)!=y)return -1;
return a[x].sum;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
a[i].v=x;
}
while(m--)
{
scanf("%d%d%d",&op,&x,&y);
if(op==0)
{
printf("%d\n",ask(x,y));
}
else if(op==1)
{
link(x,y);
}
else if(op==2)
{
cut(x,y);
}
else if(op==3)
{
splay(x);
a[x].v=y;
pushup(x);
}
}
return 0;
}
LCT 维护子树信息
众所周知,重链剖分可以利用 \(dfs\) 序来维护子树信息,那 LCT 呢?
每个点新开一个值 \(subsum\),表示这个点的所有非实边子树的信息和。
\(8\) 连虚边(也就是非实边)到 \(9\),\(1\) 连虚边到 \(2\),那么他们的 \(subsum\) 就要加上对应虚边节点的 \(sum\)。补充一下,\(subsum[2]=sum[3]+sum[4]\)。
每次 \(pushup\) 更新 \(sum\) 时,\(sum[x]\) 还要加上 \(subsum[x]\)。
在 \(access\) 里,对于 \(x,y\) 所在的两个 Splay ,其中 \(x\) 是 \(y\) 的父亲,我们要做到虚实转换,如下图:
所以,此时 \(subsum[x]\) 应计算为 \(subsum[x]=subsum[x]-sum[y]+sum[ch[x][1]]\)。
void access(int x)
{
int y=0;
while(x)
{
splay(x);
a[x].subsum+=a[a[x].c[1]].sum;
a[x].subsum-=a[y].sum;
a[x].c[1]=y;
a[y].fa=x;
pushup(x);
y=x;
x=a[x].fa;
}
}
代码2:LCT 维护子树信息(P4219大融合)
#include<bits/stdc++.h>
using namespace std;
const int maxn=300010;
int n,m,x,y,b[maxn];
char op;
struct node
{
int c[2],sum,fa,rev,subsum;
}a[maxn];
bool isroot(int i)
{
return !a[i].fa||(a[a[i].fa].c[0]!=i&&a[a[i].fa].c[1]!=i);
}
void pushup(int x)
{
a[x].sum=a[a[x].c[0]].sum+a[a[x].c[1]].sum+a[x].subsum+1;
}
void pushdown(int x)
{
if(!a[x].rev)return;
swap(a[x].c[0],a[x].c[1]);
a[a[x].c[0]].rev^=1;
a[a[x].c[1]].rev^=1;
a[x].rev=0;
}
void rotate(int x)
{
int y=a[x].fa,z=a[y].fa;
int kind=(x==a[y].c[0]);
if(isroot(y))
{
a[x].fa=a[y].fa;
}
else
{
if(a[z].c[0]==y) a[z].c[0]=x;
else a[z].c[1]=x;
}
a[y].c[kind^1]=a[x].c[kind];
a[a[x].c[kind]].fa=y;
a[x].c[kind]=y;
a[y].fa=x;
a[x].fa=z;
pushup(y); pushup(x);
}
void renew(int x)
{
if(!isroot(x))renew(a[x].fa);
pushdown(x);
}
void splay(int x)
{
renew(x);
while(!isroot(x))
{
int y=a[x].fa,z=a[y].fa;
if(!isroot(y))
{
if((a[z].c[0]==y)^(a[y].c[0]==x)) rotate(x);
else rotate(y);
}
rotate(x);
}
}
void access(int x)
{
int y=0;
while(x)
{
splay(x);
a[x].subsum+=a[a[x].c[1]].sum;
a[x].subsum-=a[y].sum;
a[x].c[1]=y;
a[y].fa=x;
pushup(x);
y=x;
x=a[x].fa;
}
}
void evert(int x)
{
access(x);
splay(x);
a[x].rev^=1;
}
int findrt(int x)
{
access(x);
splay(x);
while(a[x].c[0])
{
x=a[x].c[0];
}
return x;
}
void link(int x,int y)
{
if(findrt(x)==
findrt(y))return;
evert(x);
a[x].fa=y;
a[y].subsum+=a[x].sum;
}
void cut(int x,int y)
{
evert(y);
access(x);
splay(x);
if(a[x].c[0]==y&&!a[y].c[1])
{
a[x].c[0]=0;
a[y].fa=0;
pushup(x);
}
}
int query(int x)
{
access(x);
splay(x);
return a[x].sum;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
a[i].sum=1;
}
while(m--)
{
scanf("\n%c%d%d",&op,&x,&y);
if(op=='Q')
{
cut(x,y);
printf("%d\n",query(x)*query(y));
link(x,y);
}
else
{
link(x,y);
}
}
return 0;
}
/*
3 3
A 1 2
A 1 3
Q 1 2
*/