一看见平衡树就头重脚轻想睡觉的病怎么治/fn
预备芝士:实链剖分,即将一棵树中的边分为实边和虚边,使得每个结点连向儿子的实边只有 0 0 0 或 1 1 1 条(若有,可称其连接的儿子为首选儿子)。实边连起来形成的链就是实链。
LCT 就是把树进行实链剖分后,把每一条实链用一个 Splay 维护,Splay 的中序遍历对应树上点深度由浅到深的路径;而对于每一条虚边 f a → x fa\to x fa→x,将 x x x 所在 Splay 的根的父亲设为 f a fa fa,但 x x x 既不是 f a fa fa 的左子节点也不是 f a fa fa 的右子节点,即“认父不认子”。
我们把每一个连通块内若干 Splay 组成的结构叫辅助树,由辅助树的构造可知,一个连通块只有一个点没有父节点,就是原树的根。
利用 Splay 的灵活性质,可以在 LCT 上动态维护一些树上的加边删边问题。
下面是一堆由简单到复杂的 LCT 基本操作函数:
判断x是否为它所在Splay的根,根据认父不认子的性质就可以判断。
#define isroot(x) (x!=ch[f[x]][0]&&x!=ch[f[x]][1])
跟 Splay 差不多,只是要注意第三行必须放在最前面判,因为后续会更新 f [ y ] f[y] f[y] 导致死循环。
inline void rotate(int x){
int y=f[x],z=f[y],qwq=get(x);
if(!isroot(y)) ch[z][y==ch[z][1]]=x;
ch[y][qwq]=ch[x][qwq^1];
if(ch[x][qwq^1]) f[ch[x][qwq^1]]=y;
ch[x][qwq^1]=y,f[x]=z,f[y]=x;
pushup(y),pushup(x);
}
必须先从根开始下放所有标记再旋转,据说递归容易忘了写,所以这里使用栈:
inline void splay(int x){
int tmp=x,top=0;
while(!isroot(tmp)) st[++top]=tmp,tmp=f[tmp];
st[++top]=tmp;
while(top) pushdown(st[top--]);
for(int fa;fa=f[x],!isroot(x);rotate(x))
if(!isroot(fa)) rotate(get(fa)==get(x)?fa:x);
}
LCT 的核心操作,取出 x x x 到原树根的一条路径。实现上,把 x x x 转到当前 Splay 的根,将其首选孩子(即右子树)断开并接上已经连好的链,不要忘了时时 pushup 更新。
inline void assess(int x){
for(int y=0;x;y=x,x=f[x])
splay(x),ch[x][1]=y,pushup(x);
}
令 x x x 为它所在树的树根。先连一条 x x x 到当前根的路径,使得 x x x 和根在一个 Splay 中,然后把 x x x 转到根。结合图理解一下发现 x x x 到原根的路径上点的相对深度关系会改变,故 Splay 的中序遍历应该随之改变,所以在这里要进行区间翻转。
inline void makeroot(int x){
assess(x),splay(x),rev(x);
}
找到 x x x 所在树的根。将 x x x 换到原树根所在 Splay 的根后,根据中序遍历的性质一直往左子树走,中间不要忘了时时 pushdown。找到跟之后要 s p l a y ( ) splay() splay() 一下来保证复杂度。
inline int findroot(int x){
assess(x),splay(x),pushdown(x);
while(ls) pushdown(ls),x=ls;
return splay(x),x;
}
当 x x x 和 y y y 联通时,取出一条以 x x x 和 y y y 为两端点的实链。将 x x x 换到根之后连一条 y y y 到根的路径即可,一般为了方便后续操作,会再多一步将 y y y 转到当前 Splay 的根。
inline void split(int x,int y){
makeroot(x),assess(y),splay(y);
}
连一条 ( x , y ) (x,y) (x,y) 的边。连边之前要先判断 x x x 和 y y y 是否已经联通,可以把 x x x 转到根后判断 y y y 所在树的根是不是 x x x。若不联通,直接连一条 y → x y\to x y→x 的虚边即可。
inline void link(int x,int y){
makeroot(x);
if(findroot(y)!=x) f[x]=y;
}
断开边 ( x , y ) (x,y) (x,y)。同样的,断开之前要先判断 x x x 和 y y y 之间是否有边 (假设不能用 map),先 s p l i t ( ) split() split() 一下,此时 x x x 为原树根, y y y 为它们所在 Splay 的根,若 x x x 和 y y y 之间有边,则 x x x 必定是 y y y 的左儿子且 x x x 的右儿子为空,很显然这个能判掉 x x x 和 y y y 不在一个连通块的情况。若存在边,更新 y y y 的左儿子和 x x x 的父亲为空。
inline void cut(int x,int y){
split(x,y);
if(ch[y][0]==x&&!ch[x][1]) f[x]=ch[y][0]=0;
}
掌握了这一串函数就可以出门左转模板了qwq
传送门
神奇的用 LCT 维护边双。
考虑离线,变删边为加边,若两点不连通则直接连接,否则就是在树上缩点。这个过程可以用并查集维护。进行了 l i n k ( ) link() link() 的基本操作之后,原树根和当前 Splay 的根均为 x x x,且 x x x 和 y y y 之间已经连了实链。只要暴力递归以 x x x 的右儿子为根的子树,修改并查集,再断开 x x x 和右儿子即可。这样每个点至多被进行一次这样的修改,可以保证复杂度。
唯一要注意的是这里 a s s e s s ( ) assess() assess() 的写法要变一下, x x x 是跳到其父亲的并查集的代表元素而不是直接跳到父亲。当然其它操作也都是对并查集连通块的操作qwq
传送门
LCT 维护边的思路:把边变成点,连 u u u 和 v v v 之间编号为 i i i 的点即为连接 ( u , i ) (u,i) (u,i) 和 ( i , v ) (i,v) (i,v),删边同理。
这道题将边权从小到大排序,建出最小生成树之后再加边 ( u , v ) (u,v) (u,v) 时,每次删去连接 u u u 和 v v v 的树边中边权最小的边,再加入 ( u , v ) (u,v) (u,v),易证这样总是最优的。具体地,在 LCT 上维护最小边权和最小边的编号,查找最小值可以先记录一下每条边是否被选,找被选的编号最小的边,用线段树队列即可维护。
边按权值大小赋上编号,而点实际上没有编号,但我们可以强制给它一个编号。在 pushup 的时候如果最小值是自己,要把编号复原,否则会出现一些神秘 RE。