FHQ版LCT教程
LCT but FHQ:
邪教 ——FHQ & LCT
小言:
- 每条重链单独使用一个平衡树进行维护。
- LCT:实链剖分与平衡树维护动态树的信息,并且同时维护多个动态树,所以全局是森林。
- 每个节点认定一个实儿子,然后把这条边看成实边,其他为虚边,然后忽视虚边,维护一堆链,如果需要别的操作,虚实边可以随时转换。
- 通过一个个连边建树来建图,开始连的都是虚边,在以后通过 \(\operatorname{access}\) 函数转实边,时间复杂度看似很大,实际不错。
概念:
- 原树:原来的多叉树,方便观察原树形态。
- 辅助树:把一整个平衡树看成一个整块,用虚边连起来这些整块,方便观察各个平衡树形态。一坨实链里是平衡树(二叉),然后虚边连了好几坨(多叉)
为了方便,我把下面的单个平衡树(一坨实链)全说成辅助树了。
结合代码讲解:
其实 LCT 就像个暴力算法,大胆去写,下面未解释区域是因为自己推导并不难。
注意:
\(\operatorname{link(x,y)}\) 和 \(\operatorname{cut}(x,y)\) 如果不保证合法,就都要判合法。
……—— FHQ 定义
bool fx[300030];
struct treap {int l,r,fa,sui,xu,v,x;bool lan;} t[300030];
struct node {int x,y;};
\(\operatorname{pushdown}\) —— FHQ 下传懒标记
懒标记之翻转:用于 \(\operatorname{access}\),是必写的,同时也是为什么只能用 FHQ 和 splay 的原因。
void pushdown(int o)
{
if(t[o].lan) swap(t[o].l,t[o].r),t[t[o].l].lan^=1,t[t[o].r].lan^=1,t[o].lan=0;
}
\(\operatorname{updata}\) —— FHQ 更新
这里的异或是题目要求异或和。
inline void updata(int o)
{
t[o].x=t[o].v^t[t[o].l].x^t[t[o].r].x;
if(t[o].l) t[t[o].l].fa=o;
if(t[o].r) t[t[o].r].fa=o;
}
\(\operatorname{hebing}\) —— FHQ 合并
int hebing(int x,int y)
{
if(!x||!y) return x+y;
if(t[x].sui<t[y].sui) return pushdown(x),t[x].r=hebing(t[x].r,y),updata(x),x;
else return pushdown(y),t[y].l=hebing(x,t[y].l),updata(y),y;
}
\(\operatorname{isroot}\) —— 是否是辅助树树根
bool isroot(int o) {return (t[t[o].fa].l!=o&&t[t[o].fa].r!=o)||!t[o].fa;}}
\(\operatorname{findroot}\) —— 找辅助树的根
这里的 fx 数组同时记录分裂时的方向,因为要按 o 的位置分裂,所以用分裂要先用这个函数。
int findroot(int o)
{
top=0;
while(!isroot(o)) fx[++top]=(t[t[o].fa].l==o),o=t[o].fa;
return o;
}
\(\operatorname{findleft}\) —— 找当前辅助树中中序最小的
本质上是找辅助树在原树上深度最小的节点。
int findleft(int o)
{
o=findroot(o),pushdown(o);
while(t[o].l) o=t[o].l,pushdown(o);
return o;
}
另一种写法直接省略此函数,直接用辅助树的根的 fa 数组为原树深度最小的节点的父亲(fa 数组是 FHQ 的),如果按此方法写,可以用 \(\operatorname{findroot}\) + fa 数组解决。
复杂度是一样的,因为 \(\operatorname{findroot}\) 和 \(\operatorname{findleft}\) 复杂度一样。
但是 FHQ 貌似只能用 \(\operatorname{findleft()}\),splay 一般维护 fa 数组。
\(\operatorname{split}\) —— 改的 FHQ 分裂
在当前辅助树中把位置 \(\le o\) 的分裂为左,余为右。
关于如何找 \(o\) 的位置,我们在分裂前必须用 \(\operatorname{findroot}\) 来记录数组。
void split(int o,int &l,int &r)
{
if(!top) return pushdown(o),l=o,r=t[o].r,t[o].r=0,updata(o),void();
bool d=fx[top--];
d^=t[o].lan,pushdown(o);
if(d) r=o,split(t[o].l,l,t[o].l);
else l=o,split(t[o].r,t[o].r,r);
updata(o);
}
\(\operatorname{access}\) —— 把当前节点到根全部改为实链
int access(int o)
{
int last=0;
while(o)
{
int shang,xia;
split(findroot(o),shang,xia);
t[findleft(last)].xu=0;
last=hebing(shang,last);
t[findleft(xia)].xu=o;
o=t[findleft(last)].xu;
}
return last;
}
\(\operatorname{root}\) —— 找当前节点所在原树的根
int root(int o) {return findleft(access(o));}
\(\operatorname{changroot}\) —— 把当前节点改为原树的根
void changeroot(int o) {t[access(o)].lan^=1;}
\(\operatorname{link}\) —— 连 x 到 y 的边
void link(int x,int y) {changeroot(x),t[x].xu=y;}
\(\operatorname{cut}\) —— 切断 x 到 y 的边
void cut(int x,int y) {changeroot(x),access(y),access(x),t[y].xu=0;}
\(\operatorname{query}\) —— 查询
这个代码是查询 x 到 y 的路径上的 xor 和:
int query(int x,int y) {return changeroot(x),access(y),t[findroot(y)].x;}
\(\operatorname{change}\) —— 修改
这个代码是将点 x 上的权值变成 y。
void change(int o,int v)
{
int o1,o2;
changeroot(o);
split(findroot(o),o1,o2);
t[o].v=v,hebing(o1,o2);
}
\(\operatorname{solve}\)
cin>>n>>m;
for(int i=1;i<=n;++i) cin>>t[i].v,t[i].x=t[i].v,t[i].sui=rand();
for(int i=1,op,x,y;i<=m;i++)
{
cin>>op>>x>>y;
if(op==0) cout<<query(x,y)<<'\n';
else if(op==1&&root(x)!=root(y)) link(x,y);
else if(op==2) cut(x,y);
else if(op==3) change(x,y);
}
优秀的时间复杂度
splay 时间复杂度 \(O(n \log n)\),常数 \(≈11.4514\)。
由于实现只要能维护翻转操作就行,所以可以选择 FHQ,FHQ 常数更小了,但是时间复杂度是 \(O(n \log ^2n)\),写个快读表现就和 splay 差不多了。
关于 splay LCT 时间复杂度的严谨证明。
参照这个,我们发现访问虚边的势能分析没影响,但是 FHQ 无法均摊平衡树复杂度,所以时间复杂度要把 FHQ 的 \(O( \log n)\) 和跳链的 \(O( \log n)\),最后证明是 \(O(n \log ^2n)\)。
LCT 应用:
1. 动态树问题。
无需多言。
2. 大部分树链剖分操作。
好处:
- 在维护链用 splay 理论复杂度甚至更快,但是常数很大!树剖非理论复杂度很快。
- 比树剖套数据结构的代码短。
坏处:
- 维护子树难
最好学的 LCT 动态树无法修改子树!令人伤心。
至于怎么路径改子树查……
蒟蒻我写 FHQ 维护写挂了,没调出来,思路大概是每个点用 set 维护虚子树信息,然后查询把所有儿子都变成虚儿子后查自己和 set 的信息并。
我知道我的说法并不好,建议大家另找别的博客学习维护子树,抱歉。
3. 支持删除边的并查集(需保证无环)。
找根操作。
4. 可在线维护边权。(最小生成树之类)。
看题理解吧。
5. 在线加边维护边双联通分量。
在 \(\operatorname{link}\) 的时候,如果发现 \(x\) 和 \(y\) 在一个树里,有环,那肯定要开始缩了,锁点的时候,把这个环上的所有点全部锁到这个辅助树的根节点,因为可以把根节点的 \(fa\) 设为原树父亲,比较特殊,所以缩这里。
根节点为标志节点,用并查集代表当前节点被锁到哪去了,然后把当前环上的点提出来一个辅助树,然后递归这个辅助树更新并查集为标志节点,最后断开标志节点与子树的连接,同时在需要访问原树的父亲节点的时候,需要套用并查集,因为这个点的父亲可能已经被缩了。
函数递归缩点:
void del(int x,int y) {if(x) bcj[x]=y,del(lc,y),del(rc,y);}
找爸爸:
\(fa(x)←find(fa_x)\)
6. 维护树上染色联通块。
7. 求 LCA
inline int access(int x)
{
int y = 0;
while (x) { splay(x); Rs(x) = y; pushup(x); x = Fa(y = x); }
return y;
}
int lca = (access(u), access(v));
我们拉完 \(u\) 到根的实链后再拉 \(v\) 的。则最后一个需要调整的实链顶对应的结点自然是 \(lca(u,v)\)。
在动态树上,你想写 LCA:
忍住别写:
- 倍增 LCA:动态树上不可离线。
- 在动态树上跳链 LCA:在动态树上复杂度是均摊的,普通跳链复杂度不对。(\(\operatorname{access}\) 其实也是跳链,但是它一路上变了好多实链,这是与其不一样的地方)。
LCT 的痛点:
1. 无法子树修改
只有 TopTree 可以子树修改。
2. 原树根不固定
你不要自以为是地 \(\operatorname{access}\)!
std
#include<bits/stdc++.h>
using namespace std;
int n,m,top;
bool fx[300030];
struct treap {int l,r,fa,sui,xu,v,x;bool lan;} t[300030];
void pushdown(int o)
{
if(t[o].lan) swap(t[o].l,t[o].r),t[t[o].l].lan^=1,t[t[o].r].lan^=1,t[o].lan=0;
}
void updata(int o)
{
t[o].x=t[o].v^t[t[o].l].x^t[t[o].r].x;
if(t[o].l) t[t[o].l].fa=o;
if(t[o].r) t[t[o].r].fa=o;
}
int hebing(int x,int y)
{
if(!x||!y) return x+y;
if(t[x].sui<t[y].sui) return pushdown(x),t[x].r=hebing(t[x].r,y),updata(x),x;
else return pushdown(y),t[y].l=hebing(x,t[y].l),updata(y),y;
}
bool isroot(int o) {return (t[t[o].fa].l!=o&&t[t[o].fa].r!=o)||!t[o].fa;}
int findroot(int o)
{
top=0;
while(!isroot(o)) fx[++top]=(t[t[o].fa].l==o),o=t[o].fa;
return o;
}
void split(int o,int &l,int &r)
{
if(!top) return pushdown(o),l=o,r=t[o].r,t[o].r=0,updata(o),void();
bool d=fx[top--];
d^=t[o].lan,pushdown(o);
if(d) r=o,split(t[o].l,l,t[o].l);
else l=o,split(t[o].r,t[o].r,r);
updata(o);
}
int findleft(int o)
{
o=findroot(o),pushdown(o);
while(t[o].l) o=t[o].l,pushdown(o);
return o;
}
int access(int o)
{
int last=0;
while(o)
{
int shang,xia;
split(findroot(o),shang,xia);
t[findleft(last)].xu=0;
last=hebing(shang,last);
t[findleft(xia)].xu=o;
o=t[findleft(last)].xu;
}
return last;
}
int root(int o) {return findleft(access(o));}
void changeroot(int o) {t[access(o)].lan^=1;}
void link(int x,int y) {changeroot(x),t[x].xu=y;}
void cut(int x,int y) {changeroot(x),access(y),access(x),t[y].xu=0;}
int query(int x,int y) {return changeroot(x),access(y),t[findroot(y)].x;}
void change(int o,int v)
{
int o1,o2;
changeroot(o);
split(findroot(o),o1,o2);
t[o].v=v,hebing(o1,o2);
}
void read(int &x)
{
x=0;
char c=getchar();
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;++i) read(t[i].v),t[i].x=t[i].v,t[i].sui=rand();
for(int i=1,op,x,y;i<=m;i++)
{
read(op),read(x),read(y);
if(op==0) printf("%d\n",query(x,y));
else if(op==1&&root(x)!=root(y)) link(x,y);
else if(op==2) cut(x,y);
else if(op==3) change(x,y);
}
return 0;
}