「进行中」Dynamic Minimum Spanning Tree
XHR 问我关于动态 MST 的问题,于是找论文看到一个很神奇的 O(sqrt(m))
per update 的方法。把思路简单说一下吧。(你可以简单地把这篇文章看成是翻译 = =||)
原论文标题是 Data Structures for On-Line Updating of Minimum Spanning Trees, with Applications
,作者是 Greg N. Frederickson
。
保证每个点的度
保证每条边的权为正,这是显然的。
首先要构造一个图,使得和原图的 MST 相同,且保证每个点的度不超过 3 。
原图 G(V, E), \|V\| = n, \|E\| = m
,新图 G'(V', E'), \|V'\| = m, \|E'\| = m
,做法是将原图中的每个点拆成 D 个点(D 表示这个点的度),这 D 个点形成一个环,环上边的权为 0 ,原图的每一条边就可以对应新图一条边,而且任意两条原图的边不会占用同一个新图的点,可以保证每个点的度不超过 3 。
维护的值、可能的操作
z:块的大小
块状树的构造:
def csearch(v) :
clust = set()
for u in child[v] :
clust = cluse \| csearch(u)
if len(clust) > z :
# find a new clust
return set()
return clust
E[i, j]
:表示所有连接第 i 个块和第 j 个块的边
minE[i, j]
:表示 E[i, j]
中权最小的边
delB(i)
:删除第 i 个块。复杂度 O(z)
。
addB(S)
:建立一个块,块内的元素集合为 S 。复杂度 O(z)
。
delE(x, y)
:删除树上连接 x 和 y 的边。如果这条边连接两个块,直接无视。否则先把两个点所在的块删除,然后和相邻的某个块合并,如果合并后的块大小超过某个值,再分裂。所有访问到的块为常数个,复杂度 O(z)
。
addE(x, y)
:添加一条连接 x 和 y 的边,然后试图合并两个块。复杂度 O(z)
。
由于度数限制,每个操作的复杂度均为 O(z)
。
O(m^{2/3}) per update
我们用块状树维护 MST 。
有四种可能的操作:减小树边的权,增加非树边的权,减小非树边的权,增加树边的权。前两种总是可以忽略的。
对于第三种情况,直接查询最大的树边,有可能的话就把它替换掉。替换就是先删除一条边,再添加一条边。
对于第四种情况。我们先把这条树边删去,然后枚举所有的块对,如果可以覆盖这条边,那么用 minE[i, j]
来更新最优的非树边。最后再比较一次,如果可以更新 MST 就执行一次替换。
复杂度的事呢,第三种情况显然是 O(z)
的,第四种情况是 O(z + (m/z)^2)
的,于是 z = m^{2/3}
好了。
O(sqrt(m log m)) per update
上述做法没有用到任何数据结构维护。一个显然的优化是用某种奇怪的数据结构来优化。
Topology Tree
这里用到的数据结构是 Topology Tree 。Topology Tree 是一种动态树,他用一个奇怪的树形结构来维护原来的树,可以在 O(log m) 的时间内往原树加入、删除一条边。
考虑一棵任意点度数不超过 3 的树,我们可以 O(n)
构造它的 Topology Tree 。具体构造方式,自己参考论文吧,因为要用图来解释我就懒得搞了。
容易证明, Topology Tree 的高度是 O(log n)
的。Topology Tree 中任意一个节点的孩子数不超过 4 。
为啥这里需要 Topology Tree 呢?因为后面需要支持若干个操作: 1. 合并两棵树 2. 把一棵树分成两棵 3. 查询一棵树的最小权
块树
构造一个新树叫做“块树”,也就是把原树一个块缩为一个点。可以得到块树的大小是 O(m / z)
的。
很囧的一点,块树的度数并没有保证。于是我们要找到一个合适的划分块的方式,使得满足: 1. 在 MST 中,任意一个块至多与三个块相邻。 2. 任意一个大小小于 z 的块必定与三个块相邻。
这种划分块的方式只要在 csearch
的基础上稍稍修改即可。每次函数再多返回一个值,表示与几个块相邻。一旦块的大小超过 z 或者度数等于 3 ,这就应该要被分成一个块。可以证明,这种方式可以满足要求。(证明懒得看了)
我们把这种划分的方式成为 度限制划分
(自己随口 YY 的名字)。
具体做法
我们在度限制划分时会产生 O(m / z)
个小块,对于每个小块我们构造一棵 Topology Tree 。这棵 Topology Tree 的每个叶子表示一个划分出来的小块,顺便用一个堆按边权大小维护 E[i, j]
以及最小权的边;对于每个内节点,我们维护这棵子树内的最小权的边。很显然,所有的 O(m / z)
棵 Topology Tree 的形状是一模一样的,只是要维护的值不一样。空间复杂度易知为 O(m^2/z^2 + m)
。当 z > sqrt(m)
时,空间复杂度为 O(m)
。
再次考虑删除(添加)一个块需要做的操作。每次需要修改 O(m / z)
棵 Topology Tree 。每次修改需要修改最下层的叶子,然后依次更新内节点。由于 Topology Tree 的高度为 O(log (m / z))
,所以每次操作的复杂度为 O(m/z log (m/z))
。
考虑第三种情况。由于替换边的本质是重新维护块,而需要维护的块的个数是常数个,而且要构造常数个块的 Topology Tree ,所以复杂度是 O(m/z log (m/z) + z)
。
考虑第四种情况。这一步就可以通过维护的 Topology Tree 来加快速度。不妨设这条树边连接的是 x 和 y 。如果 x 和 y 在同一个块中,我们需要把这个块分裂成两个,这也可以通过改变常数个块来实现。在前一种算法中,我们要 O(m2/z2) 地枚举块对,现在我们只要枚举一个块,在这个块对应的 Topology Tree 中把 x-y 这条边删去,这样会产生两个 Topology Tree ,选择不包含这个块的 Topology Tree ,用维护的子树最小权的边即可更新答案。第一步的复杂度是 O(m/z log (m/z) + z)
,第二步的复杂度为 O(m/z log (m/z))
,总时间复杂度为 O(m/z log (m/z) + z)
。
均衡一下,令 z = sqrt(m log m)
可以得到整个算法 per update 的时间复杂度为 O(sqrt(m log n))
。真是太令人(理性)愉悦了~
O(sqrt(m)) per update
在上一种做法中,我们建立了 O(m / z)
棵 Topology Tree ,而这若干棵树的形态完全一样。于是我们可以考虑建立一棵“二维 Topology Tree ”来继续优化。
然后啪啦啪啦优化就是 O(sqrt(m)) 了 = =|| (具体过程正在看……)
\(O(\sqrt{n})\)