[学习笔记 #7] Link Cut Tree
[学习笔记 #7] Link Cut Tree
重学 LCT,然后被 hfu 说了。于是稍微写篇学习笔记就走人。
[ ] 里的是我还不确定的。
LCT 的基础
作用、优势 和 劣势
LCT 是一种解决动态树问题的数据结构。它可以维护森林,显著的优势是可以支持加边和删边,而劣势在于维护子树信息并不方便。
结构
- LCT 维护 森林。
- LCT 由若干棵 Splay 构成。
- 每棵 Splay 维护一条链。
- 一些 Splay 连在一起 维护 一棵树。
- 统一写到一起而不是分开写成若干个板块。
具体地,类似树链剖分,Splay 也将树剖分成若干条链,但是为了动态,链是实链而不是重链。
每个结点选择一个子结点作为实儿子,它的其他子结点作为虚儿子;[某 [个 / 些] 非叶子结点也可能会不选实儿子,此时它的子结点都是虚儿子]。
把每个点连向实儿子的边称为实边,把每个点连向虚儿子的边称为虚边。
实边连成的链叫实链,对每条实链用一个 Splay 维护。特殊地,每个落单的结点都是一条实链。
虚边连接实链。
每棵 Splay 维护的链以深度从小到大为序。
虚边连接两棵(多棵)Splay 的方式是“认父不认子”,即要把原树上这条边所连子结点的 fa 设为父结点,但父结点不把子结点纳入它的 ch 中(ch 维护的是 Splay 上的子结点)。
基本操作
实现方法不止一种,这里记我现在用的写法。
一些需要说明的:
- Splay 的根的左子树是这条链上它上面的结点,右子树是这条链上它下面的结点。
- 注意哪些地方改了边的虚实。
- 注意修改的地方是否要写 PushDown 和 PushUp。
- 我这里 PushDown 的写法是类似我平时写线段树的那种,即 PushDown 这个结点之前已经处理了这个结点的翻转(可能交换了这个结点的左右儿子)。
- LCT 不管更不维护原树长什么样。
- LCT 维护的权值在点上。
- 其他见代码。
PushUp
合并信息,上传。
类似平衡树,要管自己这个结点上的信息。
Reverse
翻转。
交换 Splay 上某个结点的左右儿子,并给翻转标记异或 \(1\)。
PushDown
下放标记。
类似线段树 [、平衡树],如果翻转标记是 \(1\) 就让左右儿子都 Reverse,将翻转标记设为 \(0\)(即清除标记)。
IsRoot
判断一个点是否是 原森林里的树根。
判断一个点是否是 它所在 Splay 的根。
根据“认父不认子”判一下即可。
PushDownAll
[[[为 Splay(操作名)做准备]]]。一个点不断往上跳,直到到了 它所在原树的根,从上往下 PushDown 回来。
递归写起来很好看,见代码。
Which
使 Rotate 写起来更简洁。判断一个点是它 在 Splay 上的父亲 的左儿子还是右儿子。
直接判,见代码。
Rotate
左旋右旋二合一,把一个点往上旋一层。
画图理解,理清顺序,不要漏掉结点,不要把一个东西改了还以为用的是原来的值,不要忘记 PushUp……注意细节。写法和其他要注意的见代码。
Splay
把一个点旋转成它所在 Splay 的根。
见代码。可能需要背一下。
“不需要背,记住就行了。”(好像是这么说的)
Access
把一个点到 它所在原树的根 的链变成实链,并且把这条链上的点连着的其他所有边都变成虚边。
不断 Splay,删边并连边,直到处理完原树的根。删边和连边通过改右儿子来一起实现,要 PushUp。见代码。
MakeRoot
把一个点变成 它所在原树 的根。
需要把这个点到 它所在原树的根 的链反向。Access, Splay, Reverse。
FindRoot
找一个点所在 原树 的根。
Access, Splay,不断向左儿子跳,跳不动就找到了。[要 Splay 上来保证复杂度]。
Link & Cut & Split
Link:(可以先判是否连通)添加一条原树上的边。
Cut:(可以先判是否有这条边)删除一条原树上的边。
Split:把一条链作为一棵 Splay 取出来,并且为了方便同时把这条链的一个端点旋到这棵 Splay 的根。
都见代码。
单点修改 & 单点查询 & 链修改 & 链查询
单点的改要先 Splay 上去,防止直接大力往上 PushUp([这样复杂度不对])。
链的直接 Split 出来,再操作。
都见代码。
维护边的信息
给原来的每条边新开一个点即可。
维护子树信息
两种。
实儿子的子树信息显然在 Splay(数据结构)中的 PushUp 就可以上传。那么还要统计虚儿子的子树信息。
对每个结点开一个值来记虚儿子的子树信息的 [并],[需要更改这个值的地方是对边的虚实有修改的地方]。
注意到修改既需要添加也需要删除。
- 如果信息有可减性,直接做。
- 如果信息没有可减性,需要用支持需要支持的删除的数据结构,比如平衡树。可以用 set,[会乘一只 \(\log\)];手写 Splay [不会乘那只 \(\log\)]。
洛谷上的模板题的代码
#include <bits/stdc++.h>
#define gc getchar
#define pc putchar
using namespace std;
namespace FastIO{
int rd()
{
int x = 0, f = 1; char c = gc();
while(c < '0' || c > '9') { if(c == '-') f = (- 1); c = gc(); }
while(c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); }
return (x * f);
}
void wt(int x)
{
if(x < 0) { x = (- x); pc('-'); }
if(x > 9) wt(x / 10);
pc(x % 10 + '0');
}
}
using namespace FastIO;
const int N = 1e5;
namespace LCT{
int fa[N + 1], ch[N + 1][2], val[N + 1], s[N + 1], rev[N + 1];
void PushUp(int u) { s[u] = s[ch[u][0]] ^ val[u] ^ s[ch[u][1]]; } // s[0] == 0
void Reverse(int u) { rev[u] ^= 1; swap(ch[u][0], ch[u][1]); }
void PushDown(int u) { if(rev[u]) Reverse(ch[u][0]), Reverse(ch[u][1]), rev[u] = 0; } // ch == 0 也没有关系
bool IsRoot(int u) { return u != ch[fa[u]][0] && u != ch[fa[u]][1]; }
void PushDownAll(int u) { if(! IsRoot(u)) PushDownAll(fa[u]); PushDown(u); }
int Which(int u) { return u == ch[fa[u]][1]; }
void Rotate(int u) { int v = fa[u], w = fa[fa[u]], wu = Which(u), wv = Which(v); if(! IsRoot(v)) ch[w][wv] = u; ch[v][wu] = ch[u][wu ^ 1]; fa[v] = u; if(ch[u][wu ^ 1]) fa[ch[u][wu ^ 1]] = v; fa[u] = w; ch[u][wu ^ 1] = v; PushUp(v); PushUp(u); }
// Rotate: 先 PushUp v 再 PushUp u;用 ! IsRoot(v),而不是直接用 w;ch[v][wu],而不是 ch[v][wu ^ 1];画图!检查!
// inline void Rotate(int x) { int y = fa[x], z = fa[y], wx = Which(x), wy = Which(y); if(! IsRoot(y)) ch[z][wy] = x; fa[x] = z; ch[y][wx] = ch[x][wx ^ 1]; if(ch[x][wx ^ 1]) fa[ch[x][wx ^ 1]] = y; ch[x][wx ^ 1] = y; fa[y] = x; PushUp(y); PushUp(x); }
void Splay(int u) { PushDownAll(u); while(! IsRoot(u)) { if(! IsRoot(fa[u])) Rotate(Which(u) == Which(fa[u]) ? fa[u] : u); Rotate(u); } } // PushDownAll // 第一个 Rotate 里的。 //
int Access(int u) { int v; for(v = 0; u; v = u, u = fa[u]) { Splay(u); ch[u][1] = v; PushUp(u); } return v; } // int ? // PushUp // u; //
void MakeRoot(int u) { Access(u); Splay(u); Reverse(u); } //
int FindRoot(int u) { Access(u); Splay(u); while(ch[u][0]) PushDown(u), u = ch[u][0]; Splay(u); return u; } // Splay
void Link(int u, int v) { MakeRoot(u); if(FindRoot(v) != u) fa[u] = v; }
void Cut(int u, int v) { MakeRoot(u); Access(v); Splay(v); if(ch[v][0] == u && ch[u][1] == 0) { ch[v][0] = fa[u] = 0; PushUp(v); } } // PushUp //
void Split(int u, int v) { MakeRoot(u); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ Access(v); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ Splay(u); }
int Query(int u, int v) { Split(u, v); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ return s[u]; }
void Modify(int u, int Val) { Splay(u); val[u] = Val; PushUp(u); }
}
using namespace LCT;
// LCT 中有的函数(比如 FindRoot 和 Access 有额外的功能)
// 编译错误不知道什么问题的时候先把 namespace LCT(三行,有一行是括号)注释掉。
void Solve()
{
int n = rd(), m = rd();
for(int i = 1; i <= n; ++ i) { int Val = rd(); Modify(i, Val); }
while(m --){
int op = rd(), x = rd(), y = rd();
if(op == 0) wt(Query(x, y)), pc('\n');
else if(op == 1) Link(x, y);
else if(op == 2) Cut(x, y);
else Modify(x, y);
}
}
int main()
{
Solve();
return 0;
}
// 注意题目中操作类型的编号从 0 开始。
LCT 的应用
动态维护一些东西。
[动态树问题]
维护最小生成树 & [瓶颈生成树]
可以支持动态加边,不能删边。只有删边的可以转换成反着加边。
可以通过维护 [瓶颈生成树] 来维护“有序的连通性”。
连边时,不连通的直接连接,连通的看要不要替换,找到路径里最劣的边,看替换掉它会不会更优,更优就替换(Cut、Link)。
找路径里最劣的边、Cut、Link 可以用 LCT 维护。
维护连通块个数
连通块用生成树来记(对每个连通块求一棵生成树)。连通块的个数等于 总点数 减去 所有生成树的边数之和。
如果是多组询问求编号在一段区间里的边形成的连通块的个数,就用扫描线,令生成树为按编号的 [瓶颈生成树]。
LCT 维护生成树,只保留了尽量存在于区间中的生成树里的边。
那么连通块的个数就是 总点数 减去 LCT 中的且在区间中的边的个数。
LCT 删边加边都是一条条加、删的。
这就转化为了二维数点,数有多少条 生成树中的边,满足它的编号在区间内。
维护树的直径(直径端点)
用好直径的性质。
当我们合并两个连通块时,得到的新直径端点一定是原来 44 个点中的两个,两两求距离更新即可。存储连通块的直径端点可以考虑并查集——from《从板子到黑题的LCT - 洛谷专栏 (luogu.com.cn)》
维护树的重心
【洛谷4299】首都(LCT维护树的重心) - TheLostWeak - 博客园 (cnblogs.com)。
题解 P4299 首都 - 洛谷专栏 (luogu.com.cn)。
-
两棵树合并时,新的重心必定在两棵树的重心间的路径上。
-
重心的各子树大小都小于等于整棵树的一半。
用 LCT 取出这条路径,在 Splay 上二分,二分完记得 Splay 保证时间复杂度。需要用 LCT 维护子树信息。
具体的二分方式我还不会。
维护(无向图的)割边
类似维护生成树,不连通的直接连,连通的(形成环)要缩点。形成环时:
- 可以不真正缩点,而是赋边权。边权为 \(1\) 表示是割边,为 \(0\) 表示不是。加边形成环就把树上这条链的边权都赋成 \(0\),正确性我忘了。(维护生成树。)
- 可以真正缩点。
维护(无向图的)割点
类似维护生成树,不连通的直接连,连通的(形成环时)因为一个割点可能属于多个点双,所以没法缩点,于是维护“圆方树”。形成环时:
- 新建一个方点,把那条链上的边都删了,连到这个方点上。
- [[[这里的圆方树允许圆点和圆点、方点和方点相连。]]]
- 关于时间复杂度的感性理解:把一条链拆成了菊花(原来链上任意两点,它们之间的距离现在都是 \(2\) 了)。
with SAM
还不会,咕咕咕。
with DP
P4115 Qtree4 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)。
参考(学习):
- 从板子到黑题的LCT - 洛谷专栏 (luogu.com.cn)。(包括这篇博客里给的题单)
- 【洛谷4299】首都(LCT维护树的重心) - TheLostWeak - 博客园 (cnblogs.com)。
- 题解 P4299 首都 - 洛谷专栏 (luogu.com.cn)。
- 动态树练习题 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)。(另一个题单)
- Link Cut Tree - OI Wiki (oi-wiki.org)。
2024.12.6
2024.12.11