简单萌萌哒 Top Tree(上)
Top Cluster 分解与 Top Tree
情景导入
我们总是想要以一种合适的方式对树进行划分,但是对于菊花图而言,基于点的划分总是不合适的,这启发我们基于边进行划分。事实上可以证明,基于边的划分总是可行的。
Top Cluster 分解就是一种基于边的划分方式,下面我们来介绍他。
簇
定义:
- 一个簇可以表示为一个三元组 \((u,v,E)\),其中 \(u,v\) 为树的节点,称为簇的界点(或端点),\(E\) 为一个边集,表示该簇包含的边,路径 \((u,v)\) 称为簇路径。
- 对于树的一条边 \((u,v)\),\((u,v,\{(u,v)\})\) 是一个簇。
- 对于簇 \(a=(u,v,E_a)\),簇 \(b=(u,w,E_b),E_a\cap E_b=\varnothing\),记 \(\text{rake}(a,b)=(u,v,E_a\cup E_b)\),也可以写成 \(\text{rake}(w)\)。
- 对于簇 \(a=(u,v,E_a)\),簇 \(b=(v,w,E_b),E_a\cap E_b=\varnothing\),记 \(\text{compress}(a,b)=(u,w,E_a\cup E_b)\),也可以写成 \(\text{compress}(v)\)。
其中 rake 和 compress 是树收缩的两种基本操作。形象的理解,一个簇可以看成一条边,rake 就是把一条边“拍”到另一条边上,compress 就是把两条边“拼”成一条;rake 是去掉一度点,compress 是去掉二度点。如下图:
显然,一棵树可以不断地进行这两种操作来合并为一个簇。一种方法是,不断地 rake 来剥掉叶子。
进行若干次收缩后,用簇路径来代替原来的边,可以得到一个树形结构(下图左侧),称为收缩树;还可以得到一棵二叉树(下图右侧),每个点代表一个簇,其两个儿子表示 compress 或 rake 操作的两个簇。我们把这棵二叉树称为 Top Tree。每个叶子节点是一条边,称为基簇;其根节点表示整棵树的簇,称为根簇。
(右图:圆点:rake 而来的;方点:不变 / compress 而来的;点上的两个小写字母表示这个簇的界点)
静态 Top Tree 的构建
为了方便问题的解决,我们希望 Top Tree 的深度是 \(O(\log n)\) 的。下面介绍一种构建方法:
首先注意到,簇的合并方式 与 轻重链剖分 有某种共性:compress 相当于重链上的 push_up,rake 相当于轻子树信息的合并。所以我们对原树进行轻重链剖分,dfs 地自底向上处理每条重链,并认为每个轻子树已经合并为一个簇。
对于重链上的每个点 \(u\),把 \(\text{fa}(u)\) 的轻子树的簇 rake 到边 \((u,\text{fa}(u))\),这里需要分治地 rake 保证深度不会太大,称这样操作形成的树为 rake 树。
然后我们得到了排成一条链的簇,同样分治地 compress 这些簇来保证深度,称这样操作形成的树为 compress 树。(称 rake 树上的点为 rake node,compress 树上的点为 compress node。)
显然,这样合并得到的 Top Tree 的深度为 \(O(\log^2 n)\)。如何优化?
根据前情提要,显然借用“全局平衡二叉树”的思想,在分治 compress 或 rake 时按簇的大小选择带权中点作为分治中点,这样 Top Tree 的深度就为 \(O(\log n)\) 了。
按这样构建出的 Top Tree 拥有一些很好的性质:
- 所有簇的界点都有祖先关系。
- 簇的上界点为整个簇中深度最小的点。
- Compress 树的中序遍历是按照深度降序遍历,从下到上。
Top Tree 是关于边的划分,而我们时常需要维护点的信息,为此我们定义:一个簇包含其连通块中的所有除上界点以外的点,这样每个点在每一层中最多属于一个簇。根节点的话可以加入一条额外边或特殊考虑来解决。
簇与 Top Tree 的性质
我们约定,如果没有特殊说明,“簇”都指的是 Top Tree 上的簇。下面列出一些比较显然但是重要的性质:
- 根据定义,簇中只有不超过 2 个点与外部点邻接,这两个点一定是界点。(重要)
- 任意两个簇,要么相互包含,要么两者的交最多只有一个点,且这个点是两个簇的共有的界点。
- 一个不是基簇的簇,其两个子簇恰好有一个共有的界点,我们称其为该簇的中心点。
- 从外部经过一个簇的路径必定经过该簇的界点。特别地,如果这个路径完全跨过了该簇,那么该簇的簇路径是其的一个子路径。
第一条的证明可以理解为,无论是 rake 还是 compress,都不影响“簇中只有不超过 2 个点与外部点邻接”这个性质。(结合第一张图理解)
Top Tree 是一棵二叉树,每个点是一个簇,父亲由儿子合并而来,叶子是一条条边的簇。
Top Tree 的结构在某种意义上类似于线段树:
- 线段树的每个节点是一个区间,最多只有两端与外部邻接,而 Top Tree 的每个节点是一个簇,最多只有两端与外部邻接;(这也是他的信息可快速合并的原因之一)
- 他们都维护子树信息并,是一种二叉分治结构,支持分治、二分,(某种意义上)是 Leafy 的;
- 树高都是 \(O(\log n)\);
- ……
所以我们可以将静态 Top Tree 直观理解为树上的线段树。
动态 Top Tree 的维护
就是支持一些动态操作的 Top Tree:
- \(\text{expose}(u,v)\):将根簇变为簇路径为 \((u,v)\) 的一个簇。
- \(\text{link}(u,v)\):在 \(u,v\) 间连边。
- \(\text{cut}(u,v)\):断开 \(u,v\) 之间的边。
接下来我们将介绍 SATT(Self-Adjusting Top Tree)(的维护方式)。
Top Tree 与原树的关系
首先,我们已经发现 compress 操作与重链上 push_up 类似,rake 操作与轻子树信息合并类似。考虑到轻重链剖分与实链剖分的关系,为了能让 Top Tree 动起来,我们先把重链替换为实链、轻边替换为虚边。
假如有一棵树,我们令一个点为根、所有儿子指向父亲,再进行实链剖分,每条实链末端为一个叶子,即所有非叶节点都恰好有一个实儿子。
这时我们再对这棵树进行 Top Tree 的构建:对于一条实链上的每个点,我们先将其所有虚子树(分治地)rake 为一个簇,再对整个链上的边分治地 compress,分治过程中若一个中心点的虚子树还未被 rake 就先 rake 到路径上。
注意除了根所在的实链,其他实链需要 compress 的边还包含其链顶向上连的虚边!
下面举个例子:
解读一下:图中 rake tree 简写为了一个大写字母。最右图中,更直观地把 rake 过程画成了两个节点挨在一起的样子。注意到任意三个同层相邻节点 \(u,A,v\) 无论是 \(uA\) 挨在一起还是 \(Av\) 挨在一起都不影响最终的结果,这样最右图可以理解为一棵三叉树。
在实际代码中,我们也常常用三叉树的结构去储存我们的 Top Tree,其中在 compress tree 中一个点的中儿子一定是一棵 rake tree 的根,中儿子也决定了这个簇的中心点。
为了统一,我们令 rake tree 也是三叉的,只不过 rake node 一定没有中儿子而已。
下面对于刚刚所说进行一些更为广泛接受的定义 + 说明:
- 指定一个度为一的点为根,并将所有边定向到它,这样的树称为 unit tree。
- 将树分解为若干不交叉的、边不相交的路径(端点可以重合在另一条路径上)。根所在的路径称为 root path,或 exposed path。除了 root path 的其他路径需要满足开始于一个叶子、结束在另一个路径上。
我们的实链加上链顶的虚边,就相当于第二条中的“路径”。
- 递归地表示整棵树的簇,我们把 root path 上每个点两侧的这些簇分别 rake 为 1 个,再将它尽可能晚地 rake 到路径上,即在这个点被 compress 前。
- 我们用一个 augmented top tree 表示这个过程。表示某点某一侧 rake 的结点在 augmented top tree 中形成了一个树,称为 rake tree。每个表示 compress 的结点最多可能有 4 个孩子,其中 2 个表示两侧 rake 到路径上的簇,2 个表示这次被 compress 的簇,所以实际最多可能表示 2 次 rake 和 1 次 compress。同样这些结点也形成了一个树,称为 compress tree。如下图:
注意到他把簇 rake 成了两个,而我们 rake 成了一个。区别在于,他这样可以维护边的顺序,例如每个点的儿子有顺序,他就可以维护这个顺序。
那么他在分治地 compress 时若一个中心点的虚子树还未被 rake,就把他 rake 到路径上。
举个例子:上图左边 \(uv,A,B,vw\) 的合并,你可以选择 \(A\) 向 \(uv\) 合并、\(B\) 向 \(vw\) 合并,也可以选择 \(A\) 向 \(vw\) 合并、\(B\) 向 \(uv\) 合并。
这样,compress tree 可以类似地直观表示为一棵四叉树,两个儿子是 compress node、两个儿子是 rake node。
但是几乎没什么题要求你维护这个顺序,所以了解即可。
再附上一张图:
- 对于原树上的一个点 \(u\),若 \(u\) 的度数 \(\ge 2\),记 \(N_u\) 表示在 \(u\) 处 compress 的簇;若 \(u\) 的度数 \(=1\),\(N_u\) 表示在 Top Tree 中最靠上的、一个界点为 \(u\) 的簇。
- 仅考虑 compress tree 的话最后的结构类似 ST-tree(LCT),rake tree 可以认为是用于维护虚子树信息。
Top Tree 的动态操作
Rotate
注意到重链剖分、LCT 之间的关系,与静态 Top Tree、SATT 之间的关系比较类似:
- LCT 本质上是把重链剖分的线段树变为可旋转、可加入删除一个 Splay 的 Splay(虚实转化),实现了动态化。
- 那么 Top Tree 是否可以支持旋转、插入删除一个簇呢?
事实上,我们发现无论是 rake tree 还是 compress tree 都可以支持旋转。
以我们的三叉 compress tree 为例,由于只要不影响其中序遍历,怎么旋都可以,所以可以这样旋:
而 rake tree 是不关心 rake 的顺序的,并且 rake node 没有中儿子,所以像上图那样旋也是没问题的。
再说四叉 compress tree 的旋转,可以这样旋(图中右旋:\(\text{rotate}(N_v)\),左旋:\(\text{rotate}(N_w)\)):
(可以对着下面这幅图理解:)
而 rake tree 中每个点只有灰色儿子,同理也可以这样旋。
注意到每个点的 rake 儿子都不变,也就是他们的中心点是不变的。
实际有可能出现 \(u\) 是父亲的 rake 儿子等的情况,这些记得判掉。
Splay
有了 Rotate 就自然有了 Splay 了,这个东西与 LCT 的基本相同。
在 compress tree 中 \(\text{splay}(u)\) 可以把 \(u\) 的中心点变成整棵 compress tree 的根簇的中心点。
Splice
在讨论 Access 操作之前,我们先介绍一个操作 \(\text{splice}(u)\),表示将簇 \(u\) 插入到他父亲的簇上。这个就相当于刚刚说的“插入删除一个簇”的操作。
我们考虑三叉树时的情况,如下图:
其中 \(x,y,z,v\) 都是簇的界点,\(vy\) 表示以 \(v,y\) 为界点的一个簇。
设 \(u\) 为 \(A,B\) 在 rake tree 上的根的父亲。
首先 \(\text{splay}(u)\) 可以使点 \(v\) 成为这个父亲的簇的中心点。
然后观察发生了什么:
图中把 rake tree 展开了一下。
发现只要 swap(xv, yv)
再更新一下信息即可!
而对于四叉树,他还需要维护儿子顺序,如下图:
原来只有簇 \(A\) 在上方、簇 \(B,vy,C\) 在下方,现在变成了簇 \(A,xv,B\) 在上方、\(C\) 在下方。
发生了什么:
仔细看的话这个图应该比较好懂,按图中这样修改就行了。
接下来只讲三叉树怎么操作的了,四叉树可以参考原论文第 5 ~ 6 页 \(\mathbf{Exposing\ the\ target.}\) 开始的一堆(实在啃不动了)
我们先对动态的 Top Tree 放宽一点要求,不要求每条实链末端为叶子了,不然讨论起来比较麻烦。
Access
\(\text{access}(x)\) 表示让点 \(x\) 与原树的根节点组成根簇的界点。
先讲过程:
记 \(x'=N_x\)。\(N_u\) 的定义在上文“Top Tree 与原树的关系”倒数第二条处。
- 若 \(x\) 不是他所在的 compress tree 的根节点的一个界点,则先 \(\text{splay}(x')\),再新建点 \(y\),令 \(x'\) 的左儿子、中儿子分别成为 \(y\) 的左儿子、右儿子,并删除 \(x'\) 的左儿子、中儿子,然后令 \(y\) 为 \(x'\) 的中儿子。
- 重复以下操作,直到 \(x'\) 到达根部(compress tree 的根节点为整棵 SATT 的根节点):
- 记此时 \(x'\) 的父亲为 \(u\),执行 \(\text{splice}(x')\).
- 若 \(u\) 是 rake node 且 \(u\) 的儿子数量小于 2,删除 \(u\)、将 \(u\) 的儿子继承到 \(u\) 的父亲。
- \(\text{splay}(x')\) 到当前 compress tree 的根的儿子。
- \(x'\gets\text{fa}(x')\)。
- 将 \(x'\) 旋转至整棵树的根。
第一步将 \(x\) 成为其所在 compress tree 对应簇的界点,称为 Local Splay。如下图:
然后如图:
这样一直重复。最后一步称为 Global Splay。
Makeroot
显然,先 \(\text{access}(x)\) 再给 \(x\) 子树打上翻转标记即可。标记下传讨论下 compress node 与 rake node 即可。
Expose
\(\text{expose}(u,v)\) 将根簇变为簇路径为 \((u,v)\) 的一个簇。
显然,先 \(\text{makeroot}(u)\) 再 \(\text{access}(v)\) 即可。
Link
\(\text{link}(x,y,z)\) 表示连边 \(x,y\),这条边在 SATT 上编号为 \(z\)。
先 \(\text{access}(x)\),再 \(\text{makeroot}(y)\),再让 \(x\) 的左儿子为 \(z\)、\(z\) 的中儿子为 \(y\) 即可。
Cut
先 \(\text{expose}(x,y)\),再删除 \(x\) 的右儿子,最后断开 \(x,y\) 即可。
图示:
于是 SATT 的基本操作就讲完了。
时间复杂度证明:不会。
SATT 维护其他信息主要都是修改 \(\text{push_up}\) 与 \(\text{push_down}\) 函数,或者通过 DFS(或者说:非局部搜索)来求,这些维护方式放到静态 Top Tree 上是完全一样的,故放到静态 Top Tree 中来讲。
整篇文章还不保熟,代码还没写,有错误请包涵并指出。
参考了、以下是一些可能有用的链接:
- Top Tree 相关理论扯淡 - ExplodingKonjac
- 静态 Top Tree 画图
- 浅谈 Top Tree - jerry3128
- Top tree 相关东西的理论、用法和实现 - 论文哥
- Sone1,从 LCT 到 SATT 的跃进 - EnofTaiPeople
- SATT 学习笔记 - F7487(就是 wiki 那篇,需要注意的是,这篇文章许多定义是与原论文等不一样的,例如其定义的 compress tree 的中序遍历是从浅到深的)
- Top Tree 树上邻域数点 - EnofTaiPeople
- 静态 Top Tree 入门 - chifan-duck(应用部分)
- 静态 Top Tree 学习笔记 - alphagem
- 原论文 - Tarjan, Werneck