我们将簇路径单独拿出来,这是一条形态特殊(为链)的树,我们为这棵树建出一棵 top tree(其代表的树收缩顺序任意)。
我们将这一结构称之为 Compress Tree,因为在这棵 Top Tree 中任一个点的两个儿子之间是通过 Compress 操作来合并成它们的父亲。
Compress Tree 里的节点称为 Compress Node。只考虑当前这条簇路径,一个非叶子的 Compress Node 就代表一次 compress 过程,表示将左儿子和右儿子信息合并起来,再将这个 compress(x) 本身存储的点 x 信息加入。这棵 Compress Tree 就维护了 C(k,g) 簇路径的信息。
另外,在 Compress Tree 中,我们实际上还对使用的 Top Tree 做了一些限制。注意到 Compress Tree 维护的是一个 T 中点的深度两两不同的链,我们规定在 Compress Tree 中基簇的中序遍历顺序与对应的 T 中边的深度是一致的,且中序遍历越小深度越浅。同样,对于每个点 x 对应的 compress(x) 的关系也是如此。
现在来维护那些非簇路径的信息,我们假设这些非簇路径上的点、边已经形成了一个个极大簇,而这些极大簇是由这些用蓝线圈出的更小簇之间互相 Rake 形成的,对由一些更小簇合并形成一个极大簇的过程,我们用一个三叉树来表示,类似地,我们称这一结构为 Rake Tree,对应地 Rake Tree 里的点就是 Rake Node。每个 Rake Node 都代表一个簇,是由其左儿子和右儿子 Rake 到其中儿子代表的更小簇上形成的。具体可见下图,可知 Rake Tree 中的每个点都代表了 T 中具有相同端点的更小簇。
上图为原树的 Rake-Compress Tree(因为每个 Rake Node 都连着一棵 Compress Tree,所以表现为一棵 Rake Tree 连着许多 Compress Tree 的形态)和代表根簇路径的 Compress Tree。
考虑将这些树以某种方式拼接在一起,使它们形成一个有序的整体。记一个 Rake Tree 代表的最小簇的集合的公共端点是点 x。我们给这些 Rake Node 的中儿子(一个 Compress Tree 集合)都加入非 x 的另一端点,但仍保持其中序遍历和 Top Tree 的基本性质,如图。
这一步相当于是让 Rake 操作加入某个 T 中点的操作直接发生在 Compress Tree 中,这不仅使我们能正确维护 Rake Node 的信息(只需将三个儿子信息合并即可),还使我们 Compress Tree 的结构更完整。下一步,我们将 Compress Tree 改为三叉树,若某个 Rake Tree 的公共端点是点 x,我们就将 Rake Tree 挂在 compress(x) 的中儿子处,如图。
此时经过三叉化的 compress(x) 点,它的意义就变成先将其中儿子 Rake 到簇路径上,再统计左右儿子和点 x 的信息。
最后,我们再处理一下根簇路径的那棵 Compress Tree:与其它所有 Compress Tree 一致地,按中序遍历加入它的两个端点,使得它的根储存整棵 T 的信息。
于是我们就实现了用三度化 Self-Adjusting Top Tree 实现一棵树的信息维护。
总结一下,SATT 有以下性质:
SATT 由 Compress Tree 和 Rake Tree 组成,Compress Tree 是一棵特殊的 Top Tree;Rake Tree 是一个三叉树,它们都对应一棵树进行树收缩的过程。
Compress Tree 里的点最多有三个儿子。Compress Tree 可以做类似于 Splay 树的旋转操作(只需保证其中序遍历不变即可,旋转一个点时保持其中儿子不动)。
Rake Tree 里的点一定有一个中儿子。Rake Tree 可以做类似于 Splay 树的旋转操作(只需保证其中序遍历不变即可,旋转一个点时保持其中儿子不动)。
在 SATT 中,有一个 access(x) 的操作,它的作用是使某点 x 成为根簇的非根端点,同时在 SATT 中使 compress(x) 成为 SATT 的根。
我们可以通过 access(x) 操作以均摊 O(logn) 的复杂度使 SATT 中代表 compress(x) 的点旋到整棵 SATT 的树根,根据 SATT 的第四个性质,我们改变了 compress(x) 的操作顺序,使得它最晚执行,x 点的信息也就被最晚加入;这样当我们要修改 x 点的信息时,就只需要更新 compress(x)。
代码实现
Push 类函数
首先考虑上传信息,即 Pushup(x) 函数。在考虑对 SATT 的某个节点维护信息时,首先分这个点在 Compress Tree 还是在 Rake Tree 进行讨论,原因可见上文,不再赘述,下面以维护某个点的子树大小为例
查询点 x 的子树大小,就将其 Access 到 SATT 根,答案是其中儿子的 size +1;因为根据上文,在 Access 之后,其中儿子才是它的真子树。
然后考虑下传信息,即 Pushdown(x) 函数。我们如果要对原树中的某个子树做整体修改,一个很自然的想法是:将这个节点直接 Access 到 SATT 根节点,给它的中儿子打上一个标记即可。同理,查询子树就直接 Access 后查询中儿子。
我们如果要对原树中的某条路径做整体修改,我们就 expose 路径的两个端点,其中 expose(x, y) 是指使点 x 成为 T 的根节点,使点 y 成为根簇的另一个端点。对应在 SATT 上,此时根簇的 Compress Tree 就是 x 到 y 的路径。于是直接给根簇的 Compress Tree 打上一个标记即可。同理查询链 expose 后查询根节点即可。
我们知道 SATT 中的 Rake Tree 和 Compress Tree 都是可以旋转的,也就是说它们可以用 Splay 来维护。因此我们可以写出以下代码:
// 是一个节点的中儿子或无父亲// ls 一个SATT节点的左儿子// rs 一个SATT节点的右儿子// ms 一个SATT节点的中儿子// type==1 在 Rake Tree中// type==0 在 Compress Tree中bool isroot(int x) { return rs(father[x]) != x && ls(father[x]) != x; }bool direction(int x) { return rs(father[x]) == x; }void rotate(int x, int type) { int y = father[x], z = father[y], d = direction(x), w = son[x][d ^ 1]; if (z) son[z][ms(z) == y ? 2 : direction(y)] = x; son[x][d ^ 1] = y; son[y][d] = w; if (w) father[w] = y; father[y] = x; father[x] = z; pushup(y, type); pushup(x, type); return;}void splay(int x, int type, int goal = 0) { pushall(x, ty); // 下传标记 for (int y; y = father[x], (!isroot(x)) && y != goal; rotate(x, ty)) { if (father[y] != goal && (!isroot(y))) { rotate(direction(x) ^ diretion(y) ? x : y, type); } } return;}
值得注意的是,函数 direction 和 isroot 与普通 Splay 的不同。因为无论这个点怎么转,这个点的中儿子是不会变的。
Access 类函数
access(x) 的意义是:将点 x 旋转到整个 SATT 的根处,使点 x 成为根簇的两个端点之一(另一端点即为 T 的根节点),同时不能改变原树的结构和原树的根。
为了实现 access(x),我们先将其旋转到其所在 Compress Tree 的树根,再把点 x 的右儿子去掉,使点 x 成为其所在 Compress Tree 对应簇的端点。
if (rs(x)) { int y = new_node(); setfather(ms(x), y, 0); setfather(rs(x), y, 2); rs(x) = 0; setfather(y, x, 2); pushup(y, 1); pushup(x, 0);}
如果这时点 x 已经到了根部,则退出;若没有,则执行以下步骤,以让它跨过它上面的 Rake Tree:
将其父亲节点(一定是一个 Rake Node),splay 到其 Rake Tree 的树根;
将 x 的爷节点(一定是一个 Compress Node)splay 到其 Compress Tree 根部。
若 x 的爷节点有一个右儿子,则将点 x 和爷节点的右儿子互换,更新信息,然后退出。
若爷节点没有右儿子,则先让点 x 成为爷节点的右儿子,此时点 x 原来的父节点没有中儿子,根据上文 Rake Node 的性质,它不能存在。于是调用 Delete 函数,将其删除,然后退出。
1,2 两个步骤合称为 Local Splay。3,4 两个步骤合称为 Splice。但我们方便起见,将它们都写在 Splice(x) 函数里。
上文提到的 Delete(x) 函数是这样的:
检视将要删除的点 x 有没有左儿子,若有,则将左儿子的子树后继续旋转到点 x 下方(成为新的左儿子),然后将右儿子(若有)变成左儿子的右儿子,此时点 x 的左儿子就代替了点 x。这相当于 Splay 的合并操作。
若没有左儿子,则直接让其右儿子代替点 x。
不难发现,Splice(x) 改变了原树的一些簇的端点选取。一次 splice 完了之后,我们将点 x 的父亲节点当作新的点 x,进行下一次 splice。
最终我们会发现,我们最开始要操作的点 x 一定在根簇的 Compress Tree 最右端。我们只需最后做一次 Global Splay,将其旋至 SATT 根部即可。
// ls 一个SATT节点的左儿子// rs 一个SATT节点的右儿子// ms 一个SATT节点的中儿子// son[x][0] ls// son[x][1] rs// son[x][2] ms// type==1 在 Rake Tree中// type==0 在 Compress Tree中int new_node() { if (top) { top--; return Stack[top + 1]; } return ++tot;}void setfather(int x, int fa, int type) { if (x) father[x] = fa; son[fa][type] = x;}void Delete(int x) { setfather(ms(x), father[x], 1); if (ls(x)) { int p = ls(x); pushdown(p, 1); while (rs(p)) p = rs(p), pushdown(p, 1); splay(p, 1, x); setfather(rs(x), p, 1); setfather(p, father[x], 2); pushup(p, 1); pushup(father[x], 0); } else setfather(rs(x), father[x], 2); Clear(x);}void splice(int x) { // local splay splay(x, 1); int y = father[x]; splay(y, 0); pushdown(x, 1); // splice if (rs(y)) { swap(father[ms(x)], father[rs(y)]); swap(ms(x), rs(y)); } else Delete(x); pushup(x, 1); pushup(y, 0);}void access(int x) { splay(x, 0); if (rs(x)) { int y = new_node(); setfather(ms(x), y, 0); setfather(rs(x), y, 2); rs(x) = 0; setfather(y, x, 2); pushup(y, 1); pushup(x, 0); } while (father[x]) { splice(father[x]); x = father[x]; pushup(x, 0); } splay(x, 0) // global splay}
若要让一个点成为原树的根,那么我们就将点 x Access 到 SATT 的根节点,可知此时点 x 已经是最终状态的簇一个端点。由 Compress Tree 的中序遍历性质可知,将点 x 所在的 Compress Tree 左右颠倒(所有点的左右儿子互换),就使点 x 成为原树的根。在具体实现中,我们通过给点 x 打上一个翻转标记,之后下传来进行这一过程。
void makeroot(int x) { access(x); push_rev(x);}
于是 expose(x, y) 就呼之欲出:
void expose(int x, int y) { makeroot(x); access(y);}
Link & Cut
现在我们要将原树中两个不连通的点之间连一条边,我们先让其中的一个点 x 成为原树的根,再将另一个点 y 旋转到根处,可知此时应该使点 y 成为点 x 的右儿子。然后在点 y 的右儿子上挂上这一条边(在只需维护点的 SATT 中,这一步可省)。
void Link(int x, int y, int z) { // z代表连接 x, y的边 access(x); makeroot(y); setfather(y, x, 1); setfather(z, y, 0); pushup(x, 0); pushup(y, 0);}
比较簇 compress(Y) 的 sum 值与簇 compress(Z)、簇 A 和点 X 的并(我们暂称为簇 α)的 sum 值。若 compress(Y) 的 sum 值大于等于后者,说明至少有一个重心在 compress(Y) 的子树中,我们递归到 compress(Y) 搜索。(如果此处取等,点 X 也是一个重心,需要记录)
比较簇 compress(Z) 的 sum 值与簇 compress(Y)、簇 A 和点 X 的并(我们暂称为簇 β)的 sum 值。若 compress(Z) 的 sum 值大于等于后者,说明至少有一个重心在 compress(Z) 的子树中,我们递归到 compress(Z) 搜索。(如果此处取等,点 X 也是一个重心,需要记录)
比较点 x 中儿子 Rake tree 之中 sum 最大的更小簇的 sum 值与簇 compress(Y)、簇 A、点 X 及其它更小簇的并(我们暂称为簇 Y)的 sum 值,若那个更小簇的 sum 值大于等于后者,说明至少有一个重心在那个更小簇的子树中,我们递归到它搜索。如果此处取等,点 X 也是一个重心,需要记录。
若以上比较都不递归,则点 X 一定是一个重心,记录并退出。
第一步的搜索显然正确,之后应该怎么搜呢?
假如我们递归到 Y,则现在 Y 储存信息的并不完整,因为 compress(Y) 里面只存储了它自己这个簇的信息,而我们要求的是整棵树的重心。解决方法是,将之前簇的信息记录下来,在点 Y 上比较计算时将上一个簇的信息与点 Y 自己的信息合并处理。具体实现如下:
#include <algorithm>#include <iostream>#define ls(x) T[x][0]#define rs(x) T[x][1]#define ms(x) T[x][2]using namespace std;constexpr int MAXN = 600005;int T[MAXN][3], tot, r[MAXN], top, st[MAXN], f[MAXN], maxs[MAXN], ss[MAXN];int nnd() { if (top) { top--; return st[top + 1]; } return ++tot;}bool isr(int x) { return rs(f[x]) != x && ls(f[x]) != x; }bool dir(int x) { return rs(f[x]) == x; }void psr(int x) { if (!x) return; r[x] ^= 1; swap(ls(x), rs(x));}void psd(int x, int ty) { if (ty) return; if (r[x]) { psr(ls(x)); psr(rs(x)); r[x] = 0; }}void psu(int x, int op) { psd(x, op); /*不知道哪没 psd*/ if (op == 0) { ss[x] = ss[ls(x)] + ss[rs(x)] + ss[ms(x)] + 1; } else { maxs[x] = max(maxs[ls(x)], max(maxs[rs(x)], ss[ms(x)])); ss[x] = ss[ls(x)] + ss[rs(x)] + ss[ms(x)]; }}void upd(int x, int ty) { if (!isr(x)) upd(f[x], ty); psd(x, ty);}void stf(int x, int fa, int ty) { if (x) f[x] = fa; T[fa][ty] = x;}void rtt(int x, int ty) { int y = f[x], z = f[y], d = dir(x), w = T[x][d ^ 1]; if (z) T[z][ms(z) == y ? 2 : dir(y)] = x; T[x][d ^ 1] = y; T[y][d] = w; if (w) f[w] = y; f[y] = x; f[x] = z; psu(y, ty); psu(x, ty);}void spy(int x, int ty, int gl = 0) { upd(x, ty); for (int y; y = f[x], (!isr(x)) && y != gl; rtt(x, ty)) { if (f[y] != gl && (!isr(y))) rtt(dir(x) ^ dir(y) ? x : y, ty); }}void cle(int x) { ls(x) = ms(x) = rs(x) = ss[x] = r[x] = maxs[x] = f[x] = 0; st[++top] = x;}void del(int x) { stf(ms(x), f[x], 1); if (ls(x)) { int p = ls(x); psd(p, 1); while (rs(p)) p = rs(p), psd(p, 1); spy(p, 1, x); stf(rs(x), p, 1); stf(p, f[x], 2); psu(p, 1); psu(f[x], 0); } else stf(rs(x), f[x], 2); cle(x);}void spl(int x) { spy(x, 1); int y = f[x]; spy(y, 0); psd(x, 1); if (rs(y)) { swap(f[ms(x)], f[rs(y)]); swap(ms(x), rs(y)); } else del(x); psu(x, 1); psu(y, 0); rtt(rs(y), 0);}void acs(int x) { spy(x, 0); if (rs(x)) { int y = nnd(); stf(ms(x), y, 0); stf(rs(x), y, 2); rs(x) = 0; stf(y, x, 2); psu(y, 1); psu(x, 0); } while (f[x]) spl(f[x]);}void mkr(int x) { acs(x); psr(x);}void epo(int x, int y) { mkr(x); acs(y);}void lnk(int x, int y) { acs(x); mkr(y); stf(y, x, 1); psu(x, 0);}void cu(int x, int y) { epo(x, y); f[x] = ls(y) = 0; psu(y, 0);}int ans1, ans2;void non_local_search(int x, int lv, int rv, int op) { if (!x) return; psd(x, 0); if (op == 0) { if (maxs[ms(x)] >= ss[ms(x)] - maxs[ms(x)] + ss[rs(x)] + ss[ls(x)] + lv + 1 + rv) { if (maxs[ms(x)] == ss[ms(x)] - maxs[ms(x)] + ss[rs(x)] + ss[ls(x)] + lv + 1 + rv) { if (ans1) ans2 = x; else ans1 = x; } non_local_search( ms(x), ss[ms(x)] - maxs[ms(x)] + ss[rs(x)] + ss[ls(x)] + 1 + lv + rv, 0, 1); return; } if (ss[rs(x)] + rv >= ss[ms(x)] + ss[ls(x)] + lv + 1) { if (ss[rs(x)] + rv == ss[ms(x)] + ss[ls(x)] + lv + 1) { if (ans1) ans2 = x; else ans1 = x; } non_local_search(rs(x), ss[ms(x)] + 1 + ss[ls(x)] + lv, rv, 0); return; } if (ss[ls(x)] + lv >= ss[ms(x)] + ss[rs(x)] + 1 + rv) { if (ss[ls(x)] + lv == ss[ms(x)] + ss[rs(x)] + 1 + rv) { if (ans1) ans2 = x; else ans1 = x; } non_local_search(ls(x), lv, rv + ss[ms(x)] + 1 + ss[rs(x)], 0); return; } } else { if (maxs[ls(x)] == maxs[x]) { non_local_search(ls(x), lv, rv, 1); return; } if (maxs[rs(x)] == maxs[x]) { non_local_search(rs(x), lv, rv, 1); return; } non_local_search(ms(x), lv, rv, 0); return; } if (ans1) ans2 = x; else ans1 = x; return;}int qu[MAXN], qv[MAXN];int main() { cin.tie(nullptr)->sync_with_stdio(false); int TT; cin >> TT; while (TT--) { int n; cin >> n; tot = n; long long ANS = 0; for (int i = 1; i <= n; i++) ss[i] = 1; for (int i = 1; i < n; i++) { cin >> qu[i] >> qv[i]; lnk(qu[i], qv[i]); } for (int i = 1; i < n; i++) { cu(qu[i], qv[i]); ans1 = 0; ans2 = 0; non_local_search(qu[i], 0, 0, 0); ANS += ans1 + ans2; if (ans1) acs(ans1); if (ans2) acs(ans2); ans1 = 0; ans2 = 0; non_local_search(qv[i], 0, 0, 0); ANS += ans1 + ans2; if (ans1) acs(ans1); if (ans2) acs(ans2); lnk(qu[i], qv[i]); } cout << ANS << '\n'; for (int i = 1; i <= tot; i++) T[i][0] = T[i][1] = T[i][2] = ss[i] = r[i] = maxs[i] = f[i] = 0; tot = top = 0; } return 0;}
Reference
Robert E. Tarjan and Renato F. Werneck. 2005. Self-adjusting top trees. In Proceedings of the sixteenth annual ACM-SIAM symposium on Discrete algorithms (SODA ‘05). Society for Industrial and Applied Mathematics, USA, 813–822. DOI 10.5555/1070432.1070547