树论笔记

Dynamic Tree

前置知识:线段树

Splay

维护区间翻转,\(O(n)=10^6\)

显然,这样的操作不能用线段树来维护,因为线段树的结构是固定的,我们需要一种结构上更加灵活的数据结构

于是联想到平衡树,如果以,对于一个区间 \([l,r]\),我们只需要知道 \(l-1\)\(r+1\) 在平衡树上的位置就可以了,其中间的部分就是翻转的区间

如果翻转的区间是一颗子树,打一个标记表示子树翻转就可以了(注意第 \(x\) 个应该是在平衡树上找第 \(x\) 大)

但正常的平衡树不能使得翻转的区间总在一颗子树上,所以引出了 \(Splay\)

\(Splay\) 基于一个朴实的想法,需要操作的点如果是根那就会很方便,那么在操作时可以先把需要操作的点一路旋转到根就可以了(为了避免被卡,旋转应该有 \(6\) 种,比较繁琐)

如果涉及新的节点(比如插入),只需要找到前驱和后继,依次转到根上,发现前驱和后继之间没有点,然后就可以 \(\Theta(1)\) 插入了

于是翻转区间实际上是一样的,把前驱和后继换成第 \(l-1\) 大和第 \(r+1\) 大就会发现它们之间就是代表区间 \([l,r]\) 的子树

复杂度证明

既然 \(Splay\) 能维护区间翻转这种线段树做不到的事,那它显然也可以维护线段树能维护的信息(区间和、区间max之类的),只需要转出区间打 \(tag\),旋转的时候 \(pushup\) 一下就好了

LCT

换根,维护 \(LCA\)

实际上可以看做求一个无根树上的‘Y’字形的拐点位置,而拐点处的点是距离'Y'字三点距离和最小的点,也就是

\[dis(a,b)=\text{a,b在树上的距离}\\ \text{设}f(x)=(dis(u,x)+dis(v,x)+dis(root,x))\\ f_{min}=f(LCA(u,v)) \]

特判一下共线,随便取一条链(以任意一个点为根),三分找拐点(显然是单峰的)

于是就有了 \(\Theta(n\log n)\) 预处理,\(O(\log n)\) 在线回答的方法了

不过上面的和 \(LCT\) 没有什么关系

为了体现 \(LCT\) 是有用的,我们加入维护:链加,链求和 的操作,这样静态树的做法就需要打树剖了,复杂度达到了 \(O(\log^2 n)\)

\(LCT\) 解决上述问题是均摊 \(O(\log n)\)

为了做到单 \(\log\) 的复杂度,我们必须摒弃轻重链剖分,转而使用一种可以使得我们需要维护的链上只有 \(O(1)\) 个区间需要用数据结构维护的新的剖分方式

可以证明,不存在比轻重链剖分更优秀的静态剖分方式

那只能考虑动态剖分了,也就是会更改点的重儿子(不妨继承轻重链中的称谓),那么自然也不能用线段树维护链信息了。我们依然保留轻重链的优秀性质:一条链上点的深度严格单调且连续。在此基础上,以点的深度作键值,用 \(Splay\) 维护链就可以做到 \(O(1)\) 更改重儿子了(假如我们已经知道要选择哪个轻儿子称为重儿子;当然,\(pushup\) 被认为是 \(O(1)\)的)。特殊地,为了更好的维护信息,一个点可以没有重儿子。

采用上述的方法,对于一个固定的根,我们只需要使它没有重儿子,一路暴力的把从它到根的路径上所有轻边变为重边就可以得到一条它到根的路径了,这样的操作被称为 \(access\)。(不妨回到 \(Splay\)复杂度证明再向下翻一点)

有了 \(access\),我们容易维护 链加,链求和,只需要再考虑 换根,LCA

LCA 的操作容易处理,只需要维护最深的没有改变过的节点就可以了

对于 换根 操作,我们观察发现轻边的属性完全由父节点表示,而父节点信息全部维护在 \(Splay\) 中,那么只需要把 \(access\) 之后的 \(Splay\) 整个翻转,重链上的点深度也就反转了!而根完全等价于深度最小的点,且 \(Splay\) 可以方便的找到点的深度的相对大小,于是我们就完成了 换根 的操作。这种操作被称为 \(makeroot\)

特殊地,为了更好的维护信息,一个点可以没有重儿子。

这启发我们用 \(LCT\) 维护更多问题,比如

连边,删边(保证无环),维护连通性

无环就是森林,而连通性等价于两点所在树的根是否相同,采用同样的思路,只需要把一条边中的随便一个点钦定为父亲,儿子 \(makeroot\) 后直接连上去就可以了

Top Tree?

我也不知道是不是 \(Top\ Tree\)

树剖可以维护子树操作,\(LCT\) 当然也可以(不然有什么用)

之前在 \(LCT\) 中,我们只维护了实(重)儿子的信息,我们把所有虚儿子放到一个 \(BST\) 里,要换的时候就挪出来,在 \(BST\) 上再维护所有虚儿子的信息,也就是被称为 \(LCT\) 三度化的操作。这样复杂度依然是对的(可我就不会证了)

这样的好处就是只需要维护两个差不多的数据结构就可以修改子树信息了(从轻儿子处跳上来的时候把 \(BST\) 里的标记下方一下就好了)

然后似乎就等价于 \(Top\ Tree\) 了?

还是简要介绍一下原始的 \(Top\ Tree\)

定义 \(Top\ Cluster\) 为一个树上点集,所有 \(cluster\) 的交为整颗树,任意一个 \(cluster\) 至多有两个顶点和其他 \(cluster\) 公用,这样的顶点叫做界点

原本树上的边都可以看做一个 \(cluster\),最后可以把所有 \(cluster\) 合并为一个表示整颗树的 \(cluster\)。存在 \(O(n)\) 的建立方式,建立的过程中,每合并一次,就从合并后的 \(cluster\) 向合并前 \(cluster\) 连边(方向不重要),构成的树就叫 \(Top\ Tree\)

可以很 trivial的基于 \(Top\ Tree\)\(O(\log n)\) 的时间内得到任意需要的 \(cluster\),不过似乎和 \(LCT\) 的写法是等价的

ETT

另一种维护动态树的方法

一颗树的 \(Euler\ Tour\)\(dfs\) 序没有什么区别,只是名字不同,也就是从根出发,过一个点一次就在一个序列末尾加入其数据,最后的序列就是 \(ET\)

发现 \(LCT\) 即使进行了三度化也很难维护半群的信息(它要求有逆元),但在序列上维护半群是容易的,用 \(splay\) 维护序列是基础的想法

但是用 \(dfs\) 序我们只能很方便的表示出子树的区间,而对于则缺乏威力

规范一下说法,我们把一个点 \(i\)\(ET\) 上第一次出现的位置叫做 \(s_i\)

我们发现,把所有子树无序地存储实在过于低效,我们可以把 \(LCT\) 中的 \(splay\) (也就是实链)上的点记做 \(x_1,x_2,x_3\cdots x_k\),显然,它们在同一颗子树内并且深度差为 \(1\),于是,我们可以让 \(s_{x_1},s_{x_2}\cdots s_{x_k}\)\(ET\) 上也构成连续的一段

这样一条实链的修改就是在序列上对 \([s_{x_1},s_{x_k}]\) 的修改,用 \(splay\) 维护序列可以做到 \(O(\log^2 n)\) (事实上,这比 \(Top\ Tree\) 慢,但可能更容易理解)

流程就是:我们用 \(LCT\) 维护原树的结构,\(splay\)\(ET\) 上维护数据

唯一的困难就是维护 \(s_i\) 了,显然只需要关注 \(\operatorname{access}\),发现就是把一段 \(s\) 平移到另外一段的后面,区间平移可以用区间翻转做到,然后就做完了,反正 \(\operatorname{makeroot}\) 的时候也需要区间翻转

非常好写(迫真)

posted @ 2022-05-28 16:26  嘉年华_efX  阅读(157)  评论(0)    收藏  举报