Link-Cut Trees 小记
忘光啦 /dk /dk
本来 rotate 都会打错的我现在终于会打啦于是 splay 打错了
基础要点
实链剖分
- 重链剖分是我们很熟悉的一个结构,它的优秀之处在于可以将树上问题更加方便地转化为序列问题(虽然也不全是)。然而注意到重剖只适用于静态树,如果将树结构动态化那么重剖就不太行了。
- 于是引入一个更加灵活的链剖结构:实链剖分。它不依据大小,也不按照深度,虚实边是人为规定的。
- 不过我们仍然需要一个数据结构来维护实链。为了适应实链剖分的灵活,我们选择 Splay 作为辅助树。
辅助树
- 由于下面辅助树可以表示出 唯一 的原树,所以我们一般只维护这些辅助树。
- 对于一条实链,显然实链上的点深度连续,因此每棵 Splay 的 中序遍历 的顶点序列深度是递增且连续的。
- 虚边在辅助树中 认父不认子。具体的,在每棵 Splay 的根结点,其父指针(
fa
)指向 当前所在链顶在原树上的父亲。
基础操作
\(\text{access}\)
access(x)
表示 打通 一条 \(x\) 到根的一条实链,实链以 \(x\) 为底,以根为顶。- 至关重要的操作之一,基本上是所有操作的基础。
- 实现不复杂,一步步往上拉,具体分 4 步:
splay
(转到根);换儿子(换实边);pushup
(换了儿子要及时更新);跳fa
。 -
inline void access(int x) { for (int p = 0; x; x = fa[p = x]) splay(x), ch[x][1] = p, pushup(x); }
\(\text{makeRoot}\)
- 若需要求出一条路径的信息,那么需要两点位于同一条实链上。然而这两个点 可能并没有祖孙关系,导致无法在同一个 Splay 中。
makeRoot(x)
表示将 \(x\) 换为原树的根。- 实现也很简单:首先
access(x)
,这时发现这条实链和预期相比恰好是反的,于是 splay 上来后就打翻转 Tag 就行了: -
inline void makeRoot(int x) { access(x), splay(x), setRev(x); }
\(\text{split}\)
split(x,y)
表示隔离出一条以 \(x, y\) 为端点的实链。- 实现依靠前两个操作:
makeRoot(x), access(y), splay(y);
。操作之后 \(y\) 就包含了链上的信息。
\(\text{link}\)
link(x, y)
表示加入一条 \(x, y\) 之间的边。- 比较稳妥的写法:先
makeRoot(x), makeRoot(y)
,接下来如果fa[x]
是 0 那么 \(x, y\) 不连通,之间fa[x] = y
即可。
\(\text{cut}\)
cut(x, y)
表示切断连接 \(x, y\) 的边。- 比较稳妥的写法:先
split(x,y)
,如果满足ch[y][0] == x && !ch[x][1]
(说明 \(x, y\) 联通且 \(x,y\) 路径上不包含其他点)则表明 \(x,y\) 由一条边直接相连,那么 将这条实边直接断掉 即可:fa[x] = ch[y][0] = 0, pushup(y);
- 如果维护了 Splay 上的子树大小也可以根据
split(x,y)
出来的 Splay 树大小判断,如果大小为 2 说明直接相连。
其他次要操作
- 要找一个点所在原树的根,需要先
access(x), splay(x)
,根据深度递增的性质 易知原树的根必然就是以 \(x\) 为根的 Splay 的最左侧,找到后记得 splay 这个点保证复杂度。这个操作习惯称为findRoot(x)
。 - 判断两点连通性:可以
makeRoot(x)
之后用上面的方法判断findRoot(y)
是否等于 \(x\) 即可。或者用link
中的方法也行。不过对常数有要求时可以考虑并查集(如果可以)。 - 修改时需要 先 splay 上来,保证不会漏更新上面的点。
注意点
-
为了 保证复杂度,理论上每次在 Splay 中向下遍历的行为都要
splay
上来找到的点。一般而言,并不推荐在access
之外的地方使用循环遍历 Splay 树结构的行为。当然有为了优化复杂度等目的使用高明方法的例外(动态加边重心),具体问题具体分析。 -
小心操作的副作用!如上述的
findRoot
需要为了保证复杂度但却 改动了 Splay 的结构,这是一个很危险的信号。比如link
如果直接使用findRoot
判连通性的话,此时 \(y\) 不能保证还是 Splay 的根,如果不干其他补救措施(加一个splay(y)
之类的)坐以待毙的话,在需要维护虚子树的信息时就会 遗漏更新。 -
rotate
、splay
的写法略与朴素 Splay 有别,这里直接贴:#define ident(x) (x == ch[fa[x]][1]) #define isRoot(x) (x != ch[fa[x]][0] && x != ch[fa[x]][1]) inline void rotate(int x) { int y = fa[x], z = fa[y], k = ident(x); if (!isRoot(y)) ch[z][y == ch[z][1]] = x; ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y; ch[x][k ^ 1] = y, fa[fa[y] = x] = z; pushup(y), pushup(x); } void flush(int x) { if (!isRoot(x)) flush(fa[x]); pushdown(x); } inline void splay(int x) { flush(x); for (int y = fa[x]; y = fa[x], !isRoot(x); rotate(x)) if (!isRoot(y)) rotate(ident(y) == ident(x) ? y : x); }
-
如果题目有强烈法方向性要求(比如 LCA 特判、有根树等等),那么可以考虑 有根树写法的 LCT(上面是无根树写法)。具体的,没有换根 & 翻转标记;
link
直接连虚边;cut
直接断重边。少去了很多花里胡哨的操作。但是注意,写法简洁、常数减小的同时也更需要斟酌其正确性。
常用技巧
边权转点权
- 如果不问点权而是边权,怎么维护?
- 很简单,把边 拆 成一个点和两条边然后照做就行。注意防止 点对应的结点影响信息,于是可以初始化为无用值或
pushup
特判等手段规避。 - 注意现在 LCT 中点数为 \(n+m\)。
- 例题:SPOJ QTREE。
动态加边 MST
- 需要基于上面的技巧,维护边权最大值及其 位置(假设求最小生成树)。
- 现在来了一条边 \((x,y)\) 权值为 \(w\)。先
split
提取出 \(x,y\) 的路径,然后求出路径上所有边的最大权值 \(w^\prime\)。如果 \(w<w^\prime\) 说明这条 新边加上会更优,于是果断将原来那个给cut
了,link
上新边。最后得到的树就是答案。 - 例题:Luogu P4234 最小差值生成树。
动态加边维护桥
- 给边赋权:\(1\) 表示桥,\(0\) 表示不是桥。那么需要维护边权和。
- 如果一条边连接两个原本不连通的点,那么
link
上一条权值为 1 的边。 - 反之则会出现一个环,而环上 不应存在任何一个桥。那么不用
link
原本连接 \(x,y\) 的路径上的所有边都赋值为 0。 - 查询两点间桥的个数,直接就是路径求和。
- 当然也有 FlashHu 那样的 显式缩点,常数小但容易写错。
- 例题:Luogu P2542 [AHOI2005] 航线规划。
维护子树信息
-
遗憾的是 LCT 对于子树就是短板了……但补救手段也不是没有。
-
常见的处理方法额外维护 虚子树 信息,然后在原来的写法上略加改动:
inline void pushup(int x) { siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + isiz[x] + 1;//此时的 siz 包含的虚子树的大小 } inline void access(int x) { for (int p = 0; x; x = fa[p = x]) { splay(x); isiz[x] += siz[ch[x][1]]; isiz[x] -= siz[ch[x][1] = p]; // 及时切换贡献 pushup(x); } }
-
注意到上面写法维护的信息需要满足可减性。如果不满足(比如子树最值)那么可以使用些数据结构辅助维护。
动态加边维护重心
Solution 1
- 考虑启发式合并的思路:每次合并两棵的树,将小树拆成单点一个个连到大树上。由于一个点之后被合并 \(O(\log n)\) 次,所以复杂度为 \(O(n\log^2 n)\)。
- 那么怎么动态维护重心?考虑到一个性质:一棵树加一个点,重心 最大移动一个距离。于是每次加点判断一下暴力挪动即可。显然挪动次数不超过加点次数,复杂度是对的。
Solution 2
-
充分利用重心的性质:两棵树合并,新重心必然在 原来两个重心在新树上的路径上。
-
那么在
link
之后先将这个连接重心的路径split
出来,然后做 平衡树上二分。 -
具体的,需要维护好 子树大小,然后维护两个字段:
lsum
表示当前搜索区间外左侧的大小,同理定义rsum
。 -
对于当前点 \(x\),如果说
lsum
加上左子树siz
不超过一半,右边也是,那么当前点必然可以作为一个重心。 -
然而我们似乎遗漏了当前点的
isiz
,这部分会不会很大导致错误?回顾上面的性质,由于isiz
必然不在这条重链 上,于是不必考虑。 -
找到之后记得及时
splay
保证复杂度,总复杂度 \(O(n\log n)\)。// uset : 维护连通块重心的并查集 inline void update(int u, int v) { split(u = find(u), v = find(v)); int x(v), lsum(0), rsum(0), tot(siz[x]), ret(N); while (x) { pushdown(x); int lcur(lsum + siz[ch[x][0]]), rcur(rsum + siz[ch[x][1]]); if (lcur * 2 <= tot && rcur * 2 <= tot) { if (tot & 1) { ret = x; break; } else { ret = std::min(ret, x); } } if (lcur < rcur) lsum = lcur + isiz[x] + 1, x = ch[x][1]; else rsum = rcur + isiz[x] + 1, x = ch[x][0]; } splay(ret); uset[u] = uset[v] = uset[ret] = ret; }
-
例题:Luogu P4299 首都。
动态 dp
- 大致思路和重链剖分相似,适用维护子树信息的技巧维护 \(f,g\) 两个矩阵即可。
- 但要注意(广义)矩阵乘法 没有交换律,注意
pushup
时的顺序。 access
时需要撤销原来虚子树的影响,直接根据矩阵的 实际意义 做即可。注意判空。- 一般来说复杂度为 \(O(n\log n)\) 乘上矩乘的复杂度,略优于树剖。
- 例题:Luogu P5024 保卫王国
维护同色连通块
- 有些题目要求维护点的颜色(一般种类不会很多)及其相关信息(比如同色连通块大小)。
- 一个简单的想法是,对于每条边,只有在两端同色时被连上。然而被 菊花图 卡爆。
- 考虑对原树定一个根,只连父边,然后 开颜色种类个 LCT。对于每个 LCT 中的一个点,仅在其当前颜色的 LCT 中连父边。
- 然后查询时,要先砍掉当前连通块的根。因为上面只连父边导致会有一个 虚假的点(并不是这个颜色)出现在连通块中,而这个点可能 联立了其他连通块,于是不能仅仅将大小减一。实际上也不是真正要执行
cut
操作,注意到access(x)
之后当前点已经在实链上了,于是我们先findRoot(x)
找到这个根,然后这个根已经在findRoot
中splay
上来了,于是直接找这个根的右儿子就是所求。 - 例题:SPOJ QTREE6。
杂题选做
【HNOI2010】弹飞绵羊
有 \(n\) 个弹簧排成一排,每个弹簧有一个弹力系数 \(K_i\),表示从 \(i\) 可以弹到 \(i+K_i\) 的位置。\(Q\) 次操作,每次修改一个 \(K_i\),或询问从 \(i\) 开始弹几次被弹飞(弹到 \(>n\) 的位置)。
- 首先应该得认出这个 森林 的模型:对于每个点 \(i\),父节点为 \(i+K_i\),如果这个点之后被弹飞那么就是根。
- 发现这题有比较强的 方向性,考虑有根树 LCT。
- 修改就是换父亲,而查询就是这个点在原树中的 深度——也就是
access
之后得到的 Splay 的siz
。 - 当然也可以用一个虚点 \(n+1\) 作为 超级根 联立森林,这样就可以套有根 LCT 模板了。复杂度都是 \(O(n\log n)\)。
【CodeChef GERALD07】Chef and Graph Queries
给定一个 \(n\) 个点,\(m\) 条边的无向图,有 \(Q\) 次询问,每次给定区间 \([l, r]\),求只保留编号在 \([l,r]\) 内的边,形成的图的连通块个数。
- 一道神仙题(应该是 wtrl
- 首先考虑按编号将边一条条加入,无非分两种情况:这条边联通了两个不同的连通块,那么答案减一;否则这条边与其他边构成了环,那么对答案没有贡献。
- 那么如果知道了这个区间的边一个个加入,那些边构成了环就行。
- 着重思考成环边:虽然构成一个环,不过如果这个环上有些 出现较早的边因为区间性质不复存在,那么环上出现缺口,这条边仍然产生贡献。实际上一个环一旦存在一个缺口就会断开两个连通块,于是不妨只考虑 出现最早的那条边。每次转化为求一条 链(当前边尚未加入)上 编号最小的边。
- 那么考虑预处理一个 \(\text{pre}_i\) 表示第 \(i\) 条边在前面的一条边 \(\text{pre}_i\) 不存在是它会有贡献。用 LCT 维护 最大生成树。
- 最后询问可以看做是求区间 \([l,r]\) 内满足 \(\text{pre}_i<l\) 的 \(i\) 的个数。二维数点,随便搞。\(O(n\log n)\)。
本文来自博客园,作者:-Wallace-,转载请注明原文链接:https://www.cnblogs.com/-Wallace-/p/lct.html