动态树(LCT、Top Tree、ETT)

▼魔方 西西 提交于 2019-12-06 10:55:12

LCT

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;if(lc)pushrev(lc);if(rc)pushrev(rc);}

pushall

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

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

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

rotate

和一般的Splay没有什么较大的差别。
要判断一下是否有节点不存在,免得修改\(0\)的信息出现玄学问题。
判断一下当前节点的父亲是否为当前Splay的根,免得破坏认父不认子的特性。

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

splay

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

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

然后就是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);}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!