动态树算法——LCT
求点赞!!!(如果觉得我讲的还好!!!谢谢!!!)
用途
在线加边或删边、查询连通性、换根等
主要函数
access()、makeroot()、findroot()
link()、split()、cut()、splay()、rotate()、pushup()、pushdown()、isroot()、get()等
前置知识:Splay
splay 是建立在平衡树基础上的, 所谓平衡树,就是对于任意结点x来说,它的左儿子为根的子树上任意一个数都小于x,右子树为根的子树上任意一个数都大于x。
比如上面这张图:
当然,也不乏一些毒瘤的情况,例如右上的链,效率真的很低, 于是各大佬们日思夜想,tarjan大佬搞出了一个叫splay的东西。
splay就是通过旋转来改变链状结构,使查询效率变高。
例如下面这张图:(我画的好丑哈哈哈哈哈)
如果我现在进行旋转操作,把X节点放到P节点的位置上, 那么根据平衡树的规则,P>X,变为X的右子树,C>P, 仍为P的右子树,但是B就得挪位置了,因为X的右儿子改为P了。可以发现,X<B<P, 所以挂在P的左节点上, 就变成了这样:
我们可以发现,如果原树中,X是P的左儿子,那么因为X<P, 旋转后,P就一定会是X的右儿子,而原来X的右儿子(子树)B一定会是P的左儿子。
相似地,如果原树中,X是P的右儿子,那么因为X>P, 旋转后,P就一定会是X的左儿子,而原来X的左儿子(子树)B一定会是P的右儿子。
那么我们的rotate函数就可以这样写:
void rotate(int x)//X是要旋转的节点
{
int y=fa[x];//父亲
int z=fa[y];//祖父
bool k1=(ch[y][1]==x),k2=(ch[z][1]==y);//分别判断儿子方向
if(!isroot(y))//FOR LCT: 判断是否是根
ch[z][k2]=x;
fa[x]=z;//孙子变成爷爷的儿子
ch[y][k1]=ch[x][!k1];
fa[ch[x][!k1]]=y; //孙子的儿子变成爸爸的儿子
ch[x][!k1]=y;
fa[y]=x;//爸爸孙子关系调换
}
接下来就是多次旋转使节点X变为根节点。采取的是每一次取三个节点,则分成以下四种情况:
yay 终于画完了
void splay(int x, int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
{
int y=t[x].ff,z=t[y].ff;//父节点祖父节点
if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
//这就是之前对于x和y是哪个儿子的讨论
rotate(x);//无论怎么样最后的一个操作都是旋转x
}
if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}
//普通splay写法 代码来源 https://www.cnblogs.com/cjyyb/p/7499020.html
//LCT中的splay写法不同就是在于出现虚边的情况需要判断是否是节点所位于的splay的根
int isroot(int rt){//是否是根
return ch[fa[rt]][0]!=rt&&ch[fa[rt]][1]!=rt;
}
void push_up(int x)
{
sz[x]=1;
if(ch[x][0])
sz[x]+=sz[ch[x][0]];
if(ch[x][1])
sz[x]+=sz[ch[x][1]];
//recalculate size of subtrees
}
void splay(int rt)
{
int top=0,stack[MAXN];//手写栈
stack[++top]=rt;//第一个一定是根
for(int i=rt;!isroot(i);i=fa[i])
stack[++top]=fa[i];
while(top)pushdown(stack[top--]);//暴力reverse,下传lazy_tag
while(!isroot(rt)){//rt非根
int x=fa[rt],y=fa[x];//父亲和祖父节点
if(!isroot(x)){//仍然非根
if((ch[x][0]==rt)^(ch[y][0]==x))turn(rt);
else turn(x);
}
turn(rt);
}
push_up(rt);
}
呼~splay到这就结束啦~
正式LCT开始
好吧第一次写这么长的题解hhhh(当然我一共也就写过两篇xswl
Access(x)
目的:打通一条从原树的根到X节点的实链。
步骤:
1.
有一棵树,假设一开始实边和虚边是这样划分的(虚线为虚边)
那么所构成的LCT可能会长这样(绿框中为一个Splay,可能不会长这样,但只要满足中序遍历按深度递增(性质1)就对结果无影响)
现在我们要access(N),把A−N的路径拉起来变成一条Splay。
该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的实边变虚。
所以我们希望虚实边重新划分成这样。
然后怎么实现呢?
我们要一步步往上拉。
首先把splay(N),使之成为当前Splay中的根。
为了满足性质2,原来N−O的重边要变轻。(我认为说实边变虚边更为准确,仅个人观点)
因为按深度O在N的下面,在Splay中O在N的右子树中,所以直接单方面将N的右儿子置为0(认父不认子)(最后一个是“0”零,不是字母“O”哈)
然后就变成了这样——
我们接着把N所属Splay的虚边指向的I(在原树上是L的父亲)也转到它所属Splay的根,splay(I)。
原来在I下方的重(实)边I−K要变轻(同样是将右儿子去掉)。
这时候I−L就可以变重了。因为L肯定是在I下方的(刚才L所属Splay指向了I),所以I的右儿子置为N,满足性质1。
然后就变成了这样——
I指向H,接着splay(H),H的右儿子置为I。
H指向AA,接着)splay(A),A的右儿子置为H。
差不多就和大佬说的一样,个人觉得实边虚边表述更好,否则容易和树链剖分混起来。
步骤总结一下:
(循环完成)
1. 将X节点转到根
2.换儿子
3. 更新信息(push_up)
4. X节点变为自己的父亲,回到1
void access(int x)
{//将 x 与 x所在树的根 连一条链
for(int y=0;x;y=x,x=fa[x])
{
splay(x);
ch[x][1]=y;
}
}
Makeroot(x)
目的:让X节点成为原树的根
步骤:
1. access(x), 让x成为当前splay中深度最大的节点。
2. splay(x), 让x成为splay树的根
3. 反转splay, 使x成为中序遍历下的最小节点(即原树中的根节点)
void makeroot(int x)
{
access(x);
splay(x);
flag[x]^=1;
}//将 x 变为树根
Findroot(x)
目的:找到x节点在原树的根
步骤:
1. access(x), 让x节点与根节点在同一splay中
2. splay(x), 让根节点成为以x为根的splay中最左边的节点(因为根节点最小嘛)
3. 寻找最左边的节点,记得之前翻转过吗?别忘了这时候执行一下~
int findroot(int x)
{//找树根
access(x);
splay(x);
while(ch[x][0])
{
pushdown(x);
x=ch[x][0];//一直往左走
}
splay(x);//随机化保证复杂度
return x;
}
下面附上完整代码 of P2147 [SDOI2008] 洞穴勘测
#include<iostream>
#include<algorithm>
#include<cstdio>
#define MAXN 10010//数组大小
#define MAX 999999999//极值
using namespace std;
int n,m;
int ch[MAXN][2];
int fa[MAXN], flag[MAXN];
bool get(int x) { return ch[fa[x]][1]==x; }
void pushdown(int rt){//标记下传
if(!rt||!flag[rt])
return;
flag[ch[rt][0]]^=1;
flag[ch[rt][1]]^=1;
flag[rt]^=1;
swap(ch[rt][0],ch[rt][1]);
}
inline int isroot(int rt){
return ch[fa[rt]][0]!=rt&&ch[fa[rt]][1]!=rt;
}
void turn(int x)
{
int y=fa[x],z=fa[y],k=get(x);
if(!isroot(y)){
if(ch[z][0]==y)ch[z][0]=x;
else ch[z][1]=x;
}
ch[y][k]=ch[x][!k]; fa[ch[x][!k]]=y;
ch[x][!k]=y; fa[y]=x;
fa[x]=z;
}
void splay(int rt)
{
int top=0,stack[MAXN];
stack[++top]=rt;
for(int i=rt;!isroot(i);i=fa[i])
stack[++top]=fa[i];
while(top)pushdown(stack[top--]);
while(!isroot(rt)){
int x=fa[rt],y=fa[x];
if(!isroot(x)){
if((ch[x][0]==rt)^(ch[y][0]==x))turn(rt);
else turn(x);
}
turn(rt);
}
}
void access(int x)
{//将 x 与 x所在树的根 连一条链
for(int y=0;x;y=x,x=fa[x])
{
splay(x);
ch[x][1]=y;
}
}
void makeroot(int x)
{
access(x);
splay(x);
flag[x]^=1;
}//将 x 变为树根
int findroot(int x)
{//找树根
access(x);
splay(x);
while(ch[x][0])
{
pushdown(x);
x=ch[x][0];
}
splay(x);//随机化保证复杂度
return x;
}
inline void split(int x,int y)
{
makeroot(x);
access(y);
splay(y);
}
void cut(int x,int y)
{
split(x,y);
ch[y][0]=0;
fa[x]=0;
}
void link(int x,int y)
{
makeroot(x);
fa[x]=y;
}
int main()
{
char c[10];
int x,y;
scanf("%d %d",&n,&m);
while(m--)
{
scanf("%s",c);
scanf("%d %d",&x,&y);
if(c[0]=='C')link(x,y);
if(c[0]=='D')cut(x,y);
if(c[0]=='Q')
{
int tmp2=findroot(y);
if(findroot(x)==tmp2)
printf("Yes\n");
else
printf("No\n");
}
}
return 0;
}