LCT 学习笔记
简介
构建
类似重链剖分,把原树分为若干条实链,用若干条虚边连接,则我们可以发现所有实链的深度单调递增,考虑每条实链建以深度为键值的 Splay,然后对于虚边实行认父不认子,也就是每颗 Splay 的根有父亲,但它的父亲的儿子没有它。
三个常用函数
getlr(u)
判断 \(u\) 是 \(u\) 父亲的左儿子还是右儿子。
bool getlr(int u){
return (ch[fa[u]][1]==u);
}
isroot(u)
判断 \(u\) 是不是 \(u\) 所在 Splay 的根,可以利用虚边认父不认子的性质。
bool isroot(int u){
return (ch[fa[u]][0]!=u && ch[fa[u]][1]!=u);
}
up(u)
通过 \(u\) 的左右儿子更新 \(u\) 的信息。
两个 Splay 的函数
rotate(u)
对 \(u\) 进行左旋或右旋,令 \(f = fa[u], g = fa[f]\),特别注意如果 \(g\) 在令一棵 \(Splay\),那么 \(g\) 不能将 \(u\) 作为自己的儿子,其它都和常规 rotate 一样,可以对照代码理解。
void rotate(int u){
int f = fa[u], g = fa[f], o = getlr(u);
if (!isroot(f)) ch[g][getlr(f)] = u;
fa[u] = g;
ch[f][o] = ch[u][!o];
if (ch[u][!o]) fa[ch[u][!o]] = f;
ch[u][!o] = f; fa[f] = u;
up(f),up(u);
}
splay(u)
把 \(u\) 提到当前 Splay 的根,与常规 Splay 大体相同
void splay(int u){
update(u); // 这个函数后面会讲到
while (!isroot(u)){
int f = fa[u], g = fa[f];
if (!isroot(f)){
(getlr(u)==getlr(f) ? rotate(f) : rotate(u));
}
rotate(u);
}
}
两个 LCT 核心函数
access(u)
作用是建立一条从 \(u\) 在原树上的根到 \(u\) 的实链,先看代码
void access(int u){
for (int p=0; u; p = u,u = fa[u]){
splay(u); ch[u][1] = p; up(u);
}
}
首先把 \(u\) 旋转到 \(u\) 所在 Splay 的顶部,然后我们需要把 \(u \rightarrow fa[u]\) 的边改为实边,但是注意我们也需要让 \(u\) 和它一个儿子的边变成虚边,
由于我们需要 \(u\) 到 \(u\) 所在原树的根的路径,令 \(p\) 为上一次的 \(u\),当 \(p = 0\) 时,需要断开 \(u\) 到 \(u\) 右子树的边(右子树的点的深度大于 \(u\),没用);当 \(p \ne 0\) 时,那么把抛弃 \(u\) 原有的右儿子,把 \(u\) 的右儿子设成上一个 \(u\) 即可
makeroot(u)
作用是把 \(u\) 设为原树的根,先看代码
void makeroot(int u){
access(u); splay(u); tag[u] ^= 1;
}
首先,建立一条从 \(u\) 在原树上的根到 \(u\) 的实链,注意到此时 \(u\) 是 \(u\) 所在 Splay 深度最大的店,那么当我们再把 \(u\) 旋到根后,\(u\) 在中序遍历后为最后一名,那么我们把整个中序遍历反过来,不久让 \(u\) 成根了?那么直接套用文艺平衡树的方法,打 tag 即可
这里也对之前的 \(update(u)\) 函数解释,\(Splay(u)\) 前,先把 \(u\) 到根上的点 \(pushdown\)
void update(int u){
if (!isroot(u)) update(fa[u]);
down(u);
}
其它操作函数
findroot(u)
找到 \(u\) 所在原树的根
int findroot(int u){
access(u);
splay(u);
while (ls(u)) down(u), u = ls(u);
splay(u); // 平衡复杂度
return u;
}
首先 \(access(u)\),把 \(u\) 旋到根,此时我们直接找到最左的儿子就是根,注意要 \(down\),最后还要 \(splay(u)\) 来平衡复杂度(这步后面也有用)
link(u,v)
连边 \((u,v)\)
void link(int u,int v){
makeroot(u);
if (findroot(v) != u) fa[u] = v;
}
首先让 \(u\) 为原树的根,然后直接连 \(u \rightarrow v\) 的虚边
cut(u,v)
删边 \((u,v)\),不保证 \((u,v)\) 存在
void cut(int u,int v){
makeroot(u);
if (findroot(v)==u && fa[v]==u && !ls(v)){
rs(u) = fa[v] = 0;
up(u);
}
}
依旧先让 \(u\) 为原树的根,首先判断 \(u\) 和 \(v\) 连不连通
分两种情况
- \(v\) 和 \(u\) 在同一个 Splay,\(findroot(v)\) 完我们发现此时原树的根 \(u\) 为 Splay 的根,这意味着 \(u\) 无左儿子,那么如果存在 \((u,v)\) 的边,\(v\) 的深度一定是 \(2\),即 \(u\) 的右儿子一定是 \(v\),且 \(v\) 没左儿子
- \(v\) 和 \(u\) 不在同一个 Splay,\(v\) 的深度也一定是 \(2\),\(findroot(v)\) 完我们发现此时若存在边 \((u,v)\),\(v\) 必须为其所在 Splay 的根且 \(v\) 无左儿子
综上可得到上面精简的判断条件
split(x,y)
整理出 \(y \rightarrow x\) 的路径
void split(int u,int v){
makeroot(u); access(v); splay(v);
}
首先让 \(u\) 为根,然后建一条 \(u \rightarrow v\) 的实链,此时已经可以计算答案,因为 \(u \rightarrow v\) 的点都在这个 Splay 里了,但是为了平衡复杂度还是把 \(v\) 旋到根,取 \(v\) 的信息
题目
维护树链信息
树剖+线段树可以直接过,但是有单 \(\log\) 的 LCT 做法
考虑 \(up(u)\) 函数怎么实现,根据 BST 的性质,相当于 \(u\) 子树内路径的颜色 = \(u\) 左儿子子树内路径的颜色 + \(u\) 的颜色 + \(u\) 右儿子子树内路径的颜色,类似线段树区间合并,维护 \(lc[u],rc[uj\) 表示 \(u\) 子树内路径最左端的颜色和最右端的颜色,然后合并信息
void up(int u){
int res = val[ls(u)]+val[rs(u)]+1;
if (ls(u) && rc[ls(u)] == ar[u]) res--;
if (rs(u) && ar[u] == lc[rs(u)]) res--;
val[u] = res;
lc[u] = (ls(u)?lc[ls(u)]:ar[u]);
rc[u] = (rs(u)?rc[rs(u)]:ar[u]);
}
特别需要注意的是,若对 \(u\) 进行子树翻转操作,那么 \(lc[u]\) 和 \(rc[u]\) 也需 \(swap\)
维护子树信息
LCT 其实并不好维护子树信息,需要满足子树信息有可减性。
我们多维护 \(u\) 的所有虚子节点的信息,注意在一些地方更新
access
void access(int u){
for (int p=0; u; p = u,u = fa[u]){
splay(u);
// 更新信息
rs(u) = p;
up(u);
}
}
link(注意注释内容)
void link(int u,int v){
makeroot(u);
makeroot(v); // 由于需要更新, 如果没有这步,需要把整颗树都更新,十分不方便,有了这步就只用更新 v
if (findroot(v) != u){
fa[u] = v;
// 更新信息
}
}
询问时,先断 \((x,y)\),然后变成 \(x\) 和 \(y\) 的子树和,维护即可

浙公网安备 33010602011771号