LCT
维护动态树的数据结构有三:
①LCT
②ETT
③Top Tree
①LCT
小说:
-
每条重链单独使用一个平衡树进行维护。
-
LCT:实链剖分与平衡树维护动态树的信息,并且同时维护多个动态树,所以全局是森林。
-
每个节点认定一个实儿子,然后把这条边看成实边,其他为虚边,然后忽视虚边,维护一堆链,如果需要别的操作,虚实边可以随时转换。
-
通过一个个连边建树来建图,开始连的都是虚边,在以后通过 access 函数转实边,也许你觉得这时间复杂度不炸了?不急没炸。
概念:
- 原树:原来的多叉树,方便观察原树形态。
- 辅助树:把一整个平衡树看成一个整块,用虚边连起来这些整块,方便观察各个平衡树形态。(一坨实链里是平衡树(二叉),然后虚边连了好几坨(多叉))
为了方便,我把下面的单个平衡树(一坨实链)全说成辅助树了。
代码:
注意:
link(x,y)和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;};
pushdown() —— FHQ 下传懒标记
懒标记之翻转:用于 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;
}
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;
}
merge() —— 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;
}
isroot() —— 是否是辅助树树根
bool isroot(int o) {return (t[t[o].fa].l!=o&&t[t[o].fa].r!=o)||!t[o].fa;}}
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;
}
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 的),如果按此方法写,可以用 findroot() + fa[] 解决。
复杂度是一样的,因为 findroot() 和 findleft() 复杂度一样。
但是 FHQ 貌似只能用 findleft(),splay 一般维护 fa 数组。
split() —— 改的 FHQ 分裂
在当前辅助树中把位置 \(≤o\) 的分裂为左,余为右。
关于如何找 o 的位置,我们在分裂前必须用 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);
}
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;
}
root() —— 找当前节点所在原树的根
int root(int o) {return findleft(access(o));}
changroot() —— 把当前节点改为原树的根
void changeroot(int o) {t[access(o)].lan^=1;}
link() —— 连 x 到 y 的边
void link(int x,int y) {changeroot(x),t[x].xu=y;}
cut() —— 切断 x 到 y 的边
void cut(int x,int y) {changeroot(x),access(y),access(x),t[y].xu=0;}
query() —— 查询
这个代码是查询 x 到 y 的路径上的 xor 和:
int query(int x,int y) {return changeroot(x),access(y),t[findroot(y)].x;}
change() —— 修改
这个代码是将点 x 上的权值变成 y。
void change(int o,int v)
{
changeroot(o);
node tmp=split(findroot(o));
t[o].v=v,merge(tmp.x,tmp.y);
}
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);
}
优秀的时间复杂度
时间复杂度 \(O(nlogn)\),常数 \(≈11.4514\)。
以上复杂度是平衡树是 splay 的条件下,不过只要能维护翻转就行,所以可以牺牲一个 log 选择 FHQ,FHQ 常数更小了,写个快读表现就和 splay 差不多了。
splay 不会 不好使,所以我就用 FHQ 了。
LCT 使的地方:
1. 动态树问题(如话)。
2. 大部分树链剖分操作。
好处:
- 在维护链用 splay 理论复杂度甚至更快,但是常树很大!树剖不理论复杂度很快。
- 比树剖套数据结构少码了。
如果有人问我怎么维护子树:
我们就维护连通块的信息,然后把他跟他父亲的边删了,这样不就能查询子树了?
如果你问我怎么维护连通块?那我就跟你说:
目前容易维护当前节点所在实链的 sum (相当于单个平衡树),若要维护整个原树的 sum 就需要用到一些方法:
坏处:
最好学的 LCT 动态树无法修改子树!!!令人伤心
3. 支持删除边的并查集(需保证无环)。
找根操作。
4. 可在线维护边权。(最小生成树之类)。
看题!
5. 在线加边维护边双联通分量。
在 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:在动态树上复杂度是均摊的,你普通跳链复杂度不对,(access 其实也是跳链,但是它一路上变了好多实链,这是不一样的地方)。
LCT 屎的地方:
1. 忍住别想 !!! LCT 子树修改
因为只有 TopTree 可以子树修改,sone1 就是有子树修改,特别难根本写不了。
2. 原树根不固定
所以你不要自以为是地 access!
②ETT
\
③Top Tree
\

LCT
浙公网安备 33010602011771号