LCT

LCT

Upd:
一个细节:假如我们要修改某个节点的数据,那么要先把它makeroot再修改,改完之后pushup。
LCT是一种维护森林的数据结构,本质是用Splay维护实链剖分。
实链剖分大概是这样的:每个节点往一个儿子连实边,其它的儿子连虚边。
而我们用Splay维护实链剖分后的每一条实链。
因此LCT有一些基本的性质:
(1.)每一棵Splay维护树上一条直上直下的实链,且其中序遍历的点的序列的深度递增。
(2.)每个节点包含且仅包含于一棵Splay。
(3.)实边包含于Splay中,而虚边则连接两棵Splay。对于每个节点而言,它会记录它在Splay中的父亲,而play的根节点的父亲则是原树中这棵Splay代表的链的链顶的父亲。但是每个节点只会记录它在Splay中的左右儿子,并不会记录由虚边连接的儿子。(认父不认子
下面如果没有特殊说明,我们默认我们所说的树为Splay而非原树。
然后我们先来说几个预备的基本操作:

nrooot

nroot实现判断一个节点是否为该节点所在Splay的根节点。
根据认父不认子的特性,我们只需要判断该节点的父亲是否有它这个儿子即可。

int nroot(int x){return ch[fa[x]][0]==x||ch[fa[x]][1]==x;}

pushrev

pushrev实现把一个子树翻转。
在后面的操作需要用到。
具体为交换左右儿子,并给左右儿子打上翻转标记。

void pushrev(int x){swap(lc,rc),rev[x]^=1;}

pushdown

pushdown实现下放标记。
在后面的操作中我们有翻转整棵子树的标记。
有的题目可能还会有其它的标记。

void pushdown(int x){if(!rev[x])return ;rev[x]=0,pushrev(lc),pushrev(rc);}

pushall

pushall实现把某个点到该点所在Splay根节点路径上的标记全部下放。
在后面的操作需要用到。
可以用栈实现不过函数堆栈更加方便。

void pushall(int x){if(nroot(x))pushall(fa[x]);pushdown(x);}

然后我们来分析一下Splay的基本操作:
这里我们需要支持的基本上就只有rotate和splay两个操作了。

rotate

和一般的Splay没有什么较大的差别。
判断一下当前节点的父亲是否为当前Splay的根,免得破坏认父不认子的特性。

void rotate(int x)
{
    int y=fa[x],z=fa[y],k=ch[y][1]==x;
    if(nroot(y)) ch[z][ch[z][1]==y]=x;
    fa[x]=z,fa[y]=x,fa[ch[x][!k]]=y,ch[y][k]=ch[x][!k],ch[x][!k]=y,pushup(y);
}

splay

和一般的Splay没有什么较大的差别。不过我们现在只需要旋转到根节点了。
不过在splay之前需要pushall一下,典型的查询前pushdown

void splay(int x)
{
    pushall(x);
    for(int y;nroot(x);rotate(x)) if(nroot(y=fa[x])) rotate((ch[fa[y]][0]==y)^(ch[y][0]==x)? y:x);
    pushup(x);
}

然后就是LCT的基本操作了。
LCT所有的操作都依赖于一个核心操作:access。

access

access实现将原树中某个点到根的路径变为一条实链,单独拿出来做一棵Splay。
假设我们要拉的是(x)点。
首先我们把(x)Splay一下。
这时(x)的左子树中的点都会在这条实链上,而右子树的点都不在。所以我们把(rc)置为(0)。(注意认父不认子的特性)
然后我们跳到(y=fa_x)(根据上面的性质,我们跳到的实际上就是原树中(x)当前实链链顶的父亲。),把y(Splay)一下。
那么此时(y)的左子树中的点还是都在这条实链上,而右子树的点都不在。所以我们把刚才的(x)接在(y)的右儿子处。
注意因为修改后pushup的原则,我们需要在更新右儿子的时候pushup一下。
这样一直做到原树的根为止即可。

void access(int x){for(int y=0;x;x=fa[y=x])splay(x),rc=y,pushup(x);}

makeroot

相比access,makeroot更进一步地实现了把一个节点(x)转成原树中的根节点。
原理还是很简单的。我们先把(x)access一下,然后把(x)splay到它所在Splay的根节点。
此时因为它是原树中这条链上深度最大的点,所以它没有右儿子。
为了让它变成原树中深度最小的点,我们把它的左右子树交换一下,这样他就没有左儿子了,就变成了原树中深度最小的点。
交换子树可以通过打标记的方法来完成。

void makeroot(int x){access(x),splay(x),pushrev(x);}

findroot

findroot实现找到一个节点(x)所在原树中的根节点。
和makeroot类似,我们先access、splay(x)
那么此时(x)所在原树中的根就是它左儿子中一直跳左儿子最后到的点。
根据查询前pushdown的原则我们要一边跳左儿子一边pushdown。
Upd:根据xzz的说法这里由于我们pushdown的写法可以不用pushdown。
最后可以顺便把查询到的根节点给splay一下。

int findroot(int x){access(x),splay(x);while(lc)x=lc;return splay(x),x;}

split

split实现把原树中一条链((x,y))单独拿出来做一条实链。
很轻松地,我们先让(x)做原树的根,然后拉一条((x,y))的实链出来,再让(y)做这条实链的Splay的根。这样我们在过程中通过pushup和pushdown就会让这条实链(这棵Splay)上的信息反映到这棵Splay的根节点(y)上。
然后如果我们要查询一条路径的信息,就可以快乐地split然后查(y)的信息了。

void split(int x,int y){makeroot(x),access(y),splay(y);}

下面则是一些真正意义上LCT能做而树剖做不了的东西了。

link实现在两点(x,y)之间新连一条边((x,y))。(如果(x,y)连通就不连)

保证(x,y)不连通:

直接让(x)(x)所在原树的根,然后把(fa_x)置为(y)即可。

void link(int x,int y){makeroot(x),fa[x]=y;}

不保证(x,y)不连通:

还要判一下连通性。即(x)成为原树所在的根之后,判断(y)所在原树的根是否为(x)

void link(int x,int y){makeroot(x);if(findroot(y)^x)fa[x]=y;}

cut

cut实现断掉边((x,y))。(如果不存在边(x,y)就不断)。

保证((x,y))存在:

先把((x,y))给split出来。
然后根据split的写法,此时(y)一定是该Splay的根,(x)一定是该原树的根,所以(x)一定是(y)的左儿子,那么我们直接断就完事了。
记得pushup。

void cut(int x,int y){split(x,y),fa[x]=ch[y][0]=0,pushup(y);}

不保证((x,y))存在:

先判断连通性。
由于我们makeroot了,所以此时(x)一定为原树的根。
又由于我们findroot了,所以此时(x)一定是这条链的Splay的根。
那么如果此时(y)(x)的右儿子,(x)(y)的父亲,且(y)没有左儿子((x,y)之间没有其它深度的点),那么((x,y))就是存在的。
事实上如果满足了(x)(y)的父亲,(y)没有左儿子,那么((x,y))就是存在的。
因为(x)不可能有左儿子。

void cut(int x,int y){makeroot(x);if(findroot(y)==x&&fa[y]==x&&!ch[y][0])fa[y]=rc=0,pushup(x);}

这样我们就成功完成了LCT的基本操作。
然后就是一些技巧性的东西了:

LCT维护e-dcc:

删边似乎不太好做?只考虑加边(虽然树剖也能做)。
加入一条非树边时会形成一个e-dcc,把LCT上这条路径所在的Splay用并查集并成一个点即可。

LCT维护v-dcc:

只考虑加边,考虑维护圆方树。
加入一条非树边时将这段树上路径全部砍断,新建一个点代表这个点双,将原来那些点向新点连虚边即可。

LCT维护边权:

一般而言我们最基本的思路是把边权放到其深度更大的那一端。
但是由于LCT会改变父子关系所以这东西没办法做了。
因此我么考虑拆点,把一条边拆成一个点,向其两端连边。
然后就可以做了。

LCT维护子树:

这是个大头。
如果会Top Tree的话似乎会轻松很多。
对于某些较容易维护的信息,我们可以考虑开一个新的数组来记录每个点的所有轻儿子所在子树的信息和,那么这个点的子树信息和就是两个重儿子的子树信息和加上所有轻儿子的子树信息和了。
(下面的(sum)表示该节点所有轻儿子的子树信息和,(Sum)表示该节点子树的信息和)
这样对于pushup操作,我们要多加一点东西。

void pushup(int x){Sum[x]=Sum[lc]+Sum[rc]+sum[x]+1;}

对于改变了轻重关系的操作,我们需要实时维护这个东西。比如access。

void access(int x){for(int y=0;x;x=fa[y=x])splay(x),sum[x]+=Sum[rc],sum[x]-=Sum[rc=y];}

link:
(y)多了一个轻儿子,所以要进行更改。
所以(y)在Splay中的祖先也要跟着更改,这太麻烦了。
我们把(y)旋转到它所在Splay的根,就不用了改(y)的祖先了。

保证(x,y)不连通:

void link(int x,int y){split(x,y),sum[fa[x]=y]+=Sum[x],pushup(y);}

这里的split是一个偷懒的写法。

不保证(x,y)不连通:

void link(int x,int y){makeroot(x);if(findroot(y)^x)sum[fa[x]=y]+=Sum[x],pushup(y);}

cut:
断掉的一定是重边,因为我们pushup了所以并没有特殊的影响。

LCT维护染色连通块:

可以直接开一个LCT,把同色的点连起来,不过很辣鸡。
更加优秀一点的是每个颜色开一个LCT。
还有一些诸如把点的颜色赋给其父边的技巧。

原文地址:https://www.cnblogs.com/cjoierShiina-Mashiro/p/11979385.html