闲话 23.2.22
闲话
好像我做题数真挺少的?
随便找个人的切题记录我就一大半没写过的
写过的全是板子
wqz 老师都 400 紫了 %%%
当然纠结这东西也没啥意义就是了
但退役感++
今天中午的歌还行吧
没啥可评价的
soytony:今天是 b20 日
今日推歌:我在雨中旋转 - 乱心 feat. 洛天依
感觉蛮好听的 有一种随性的感觉
而且有些洗脑的(
\(\text{Link-Cut Tree}\)
突然想起可以写个这玩意水博客。
Link-Cut Tree 是一种可以动态维护有根树森林连通性以及路径相关信息的数据结构,其通过构造实链剖分的处理方法以及应用 Splay 维护单条实链,得到了单次操作理论复杂度 \(O(\log n)\) 但常数较大的做法。
下面约定 Splay
指代一类平衡树,splay
指代将 Splay 中一个节点上提到根的操作。
首先我们需要介绍实链剖分和 LCT 的性质。
实链剖分和重链剖分(HLD)相似,将一棵树划分为两两不交的链并维护整条链的信息,从而维护整棵树的形态。区别在于,重链剖分一般应用在静态树上,确定了一个划分链的具体形式,从这形式上衍生出的结构“全局平衡二叉树”就是将一条静态链的信息聚合,可以做到单次操作时间复杂度 \(O(\log n)\) 地维护路径相关信息;而实链剖分并未确定一个具体的形式,而是直接通过一个动态数据结构集合支持合并链与切割链,使得在需要信息时可以提取出一条到根的链,将链上信息聚合在同一个集合中,从而动态维护连通性。
在具体形式上,实链剖分将每个节点向某个儿子的连边定为实边,其余连边称为虚边,实边对应的儿子叫实儿子,其他儿子叫虚儿子;实边连接成极长的实链,任意时刻的静态形式和重链剖分类似,我们也维护了一条链的信息。虚/实边并不是固定的,我们可以切换一个点的实边,从而动态维护树的形态。这就需要一个高级数据结构来维护链信息,发明者 R. E. Tarjan 选择了 Splay。
任意时刻,整棵树的信息分布在数棵 Splay 构成的森林中,每棵 Splay 都维护了一条实链的信息,常有 Splay 的节点一一对应对应原森林中的节点,每个原森林中的节点仅处于一棵 Splay 中。显然这实链的节点深度严格递增,而我们保证了中序遍历一棵 Splay 得到的节点序列深度严格递增。换句话说,我们将一条实链压成了一棵平衡树。
实链顶节点需要通过虚边连接到另一条实链上它的父亲,映射到 Splay 上就是一棵 Splay 的根节点的父亲指针指向对应实链顶的父亲对应的 Splay 节点。由于一个节点只有一个实儿子,虚儿子不在父亲所在的 Splay 中,这使得这节点在 Splay 上无法直接访问到虚儿子,但虚儿子可以通过所在 Splay 根节点的父亲指针访问到这节点,这也就是 LCT“认父不认子”的性质。
下面我们要介绍一系列操作,通过这些操作我们可以在维护 LCT 的性质的同时实现我们需要的操作。
Splay 相关操作不算在内,作为前置知识。
\(\text{access}(u)\)
执行此操作,我们可以得到一条根节点到 \(u\) 节点的实链,即维护这实链信息的一棵 Splay。
可以发现,我们只需要将 \(u\) 转到 Splay 的根、舍弃掉其右子树、与它的父亲指针指向的 Splay 合并,并不断执行该操作,我们最终就能得到一棵只包含根到 \(u\) 的链的 Splay。
这过程可以自然对应到 Splay 上。前两个操作自然能通过 splay 操作和断边实现。第三个操作可以首先将父亲指针指向的节点 splay 到所在 Splay 的根,并断右子树的边,再将 \(u\) 连接到其父亲的右子树上。
可以发现这过程的两部分是彼此同构的,因此我们可以每次都执行第三个操作,过程中维护一个先前的虚儿子,初始化为 0。
过程中需要首先下放 lazy 标记。
\(\text{makert}(u)\)
执行该操作,我们可以将根节点设为 \(u\),这可以为我们得到任意路径 \(u-v\) 的信息提供方便。
根据性质不难发现,执行 \(\text{access}(u)\) 后 \(u\) 为根所在 Splay 中最右侧节点,根为同 Splay 中最左侧节点。因此我们只需要将这整棵 Splay 翻转即可。将 \(u\) splay 到根,随后在 \(u\) 节点上打翻转标记即可。
\(\text{findrt}(u)\)
执行该操作,我们可以找到森林中 \(u\) 所在树的根节点。
根据性质不难发现,执行 \(\text{access}(u)\) 后 \(u\) 为根所在 Splay 中最右侧节点,根为同 Splay 中最左侧节点。因此我们只需要将 \(u\) 旋转到根,随后一直进入节点的左儿子,直到所在节点无左儿子即可。这时所在节点即为根对应的节点。
进入左儿子过程中需要首先下放 lazy 标记;由于之后操作的性质,到达根后需要将根对应节点 splay 到 Splay 的根。
这操作可以用于判定两节点是否在同一棵树中:两节点在同一棵树中当且仅当他们的根节点相同。
实践证明该操作很慢。因此在一部分只有合并的题目中可以考虑额外维护并查集来得到连通性,以此卡常数。
\(\text{split}(u, v)\)
执行该操作,我们可以得到仅由路径 \(u-v\) 上节点组成的 Splay。
这操作是简单的,我们只需要 \(\text{makert}(u), \text{access}(v), \text{splay}(v)\) 即可。整条路径的信息包含在 \(v\) 节点中。
\(\text{link}(u, v)\)
执行该操作,我们可以在原森林中加入一条边 \((u,v)\)。
首先需要判定 \(u, v\) 不在同一棵树中,反之可以直接报告,根据题目要求特殊处理。判断可以用 \(\text{findrt}\) 简单实现。
这里钦定让 \(v\) 连一条轻边到 \(u\)。我们首先将 \(u\) 设为所在树的根,随后直接将父亲指针指向 \(v\) 即可。
\(\text{cut}(u, v)\)
执行该操作,我们可以在原森林中删除一条边 \((u,v)\)。
首先需要判定 \(u, v\) 在同一棵树中,反之可以直接报告,根据题目要求特殊处理。判断首先需要用 \(\text{findrt}\) 看是否在一棵树中。随后让 \(u\) 变为根,这时如果 \(v\) 的父亲不是 \(u\) 或者 \(v\) 有左儿子则都不合法。
判完上面的情况后一定合法,当前 \(u\) 是 Splay 的根,\(v\) 是 \(u\) 的一个儿子,深度大于 \(u\) 所以在 Splay 上是 \(u\) 的右儿子。直接断开即可。
常数不算很大的板子
struct Link_Cut_Tree {
#define ls(p) spl[p].ch[0]
#define rs(p) spl[p].ch[1]
#define sn(p, s) spl[p].ch[s]
#define fa(p) spl[p].fa
#define fl(p) spl[p].fl
#define val(p) spl[p].val
#define mnv(p) spl[p].mnv
#define pos(p) spl[p].pos
#define notrt(p) ((p == ls(fa(p))) or (p == rs(fa(p))))
struct node {
int fa, ch[2], val, mnv, pos;
bool fl;
} spl[N << 2];
inline void flip(int p) { swap(ls(p), rs(p)), fl(p) ^= 1; }
inline void ps_p(int p) {
mnv(p) = val(p), pos(p) = p;
if (ls(p) and mnv(ls(p)) < mnv(p)) mnv(p) = mnv(ls(p)), pos(p) = pos(ls(p));
if (rs(p) and mnv(rs(p)) < mnv(p)) mnv(p) = mnv(rs(p)), pos(p) = pos(rs(p));
}
inline void ps_d(int p) {
if (fl(p)) {
if (ls(p)) flip(ls(p));
if (rs(p)) flip(rs(p));
fl(p) = 0;
}
}
inline void rotate(int x) {
int y = fa(x), z = fa(y), k = rs(y) == x, son = sn(x, !k);
if (notrt(y)) sn(z, rs(z) == y) = x;
sn(x, !k) = y, sn(y, k) = son;
if (son) fa(son) = y;
fa(y) = x, fa(x) = z;
ps_p(y), ps_p(x);
}
void splay(int x) {
static int stk[N];
int z = 0, y = x;
stk[++ z] = x;
while (notrt(y)) stk[++ z] = y = fa(y);
while (z) ps_d(stk[z --]);
while (notrt(x)) {
y = fa(x), z = fa(y);
if (notrt(y)) rotate((ls(y) == x) ^ (ls(z) == y) ? x : y);
rotate(x);
} ps_p(x);
}
inline void access(int x) {
int y = 0;
while (x) {
splay(x); rs(x) = y; ps_p(x);
x = fa(y = x);
}
}
inline void makert(int x) {
access(x); splay(x); flip(x);
}
int findrt(int x) {
access(x), splay(x);
while (ls(x)) {
ps_d(x);
if (!ls(x)) break;
x = ls(x);
} return splay(x), x;
}
inline void split(int u, int v) {
makert(u), access(v), splay(v);
}
inline void link(int u, int v) {
makert(u);
if (findrt(v) != u) fa(u) = v;
}
inline void cut(int u, int v) {
makert(u);
if (findrt(v) == u and fa(v) == u and ls(v) == 0) {
fa(v) = rs(u) = 0;
ps_p(u);
}
}
#undef ls
#undef rs
#undef sn
#undef fa
#undef fl
#undef val
#undef mnv
#undef pos
#undef notrt
} lct;
通过上述操作,我们可以简单地维护带点权的森林。但当我们需要维护带边权的森林时,同样的做法就显得力不从心了。
我们可以考虑拆边为点。我们新申请一个 Splay 内节点 \(x\) 对应边 \((u, v)\),点权设为原边权,随后 \(\text{link}(u, x), \text{link}(x, v)\) 即可。正常节点的点权设为空值即可。
以下是博客签名,与正文无关。
请按如下方式引用此页:
本文作者 joke3579,原文链接:https://www.cnblogs.com/joke3579/p/chitchat230222.html。
遵循 CC BY-NC-SA 4.0 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。