一个最小生成树的经典扩展问题

有一张 n n n 个点构成的完全图,每个点有一个非负整数边权 a i a_i ai,两点 u , v u,v u,v 之间的边权为 a u xor ⁡ a v a_u \operatorname{xor} a_v auxorav,求图中边权和最小的生成树。
n ≤ 1 0 5 ,   a i ≤ 1 0 9 n\leq 10^5,~a_i\leq 10^9 n105, ai109

暴力

直接应用 Kruskal \text{Kruskal} Kruskal 算法。

O ( n 2 ) \mathcal O(n^2) O(n2) 条边全部存下来,然后按顺序加进去。

时间复杂度 O ( n 2 log ⁡ n 2 ) \mathcal O(n^2\log n^2) O(n2logn2)

算法一

考虑将所有点直接建一个 Trie \text{Trie} Trie 出来。

异或值之和较小就相当于较高的位要尽可能相同,也就是在 Trie \text{Trie} Trie 上对应的链公共部分要尽量长。换句话说,如果存在三点,其中两点在某一点的子树内,另一点在子树外,那么把子树内的两点合并后再和子树外的点合并,肯定比这两点分别与子树外的点合并更优。

所以对于一棵 Trie \text{Trie} Trie,我们应该从下往上依次合并左右子树。在左右子树内各选一个点,使得两者的点权异或值最小。

考虑直接枚举较小的那棵子树的所有点,然后每个点都到另外一棵子树中查询,最后取一个最小值即可。

相当于一个启发式合并的过程,每次合并都会使需要枚举的子树大小至少扩大一倍,最多扩大 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 次,所以总时间复杂度就是 O ( n log ⁡ n log ⁡ A ) \mathcal O(n\log n\log A) O(nlognlogA) A = max ⁡ { a i } A=\max \{a_i\} A=max{ai})。

如果不用启发式合并其实也没有问题,因为每个点最多被枚举 O ( log ⁡ A ) \mathcal O(\log A) O(logA) 次,所以这样的时间复杂度是 O ( n log ⁡ 2 A ) \mathcal O(n\log^2 A) O(nlog2A)

算法二

考虑先将 { a i } \{a_i\} {ai} 排序,然后从高位到低位分治。

假设当前考虑到 2 i 2^i 2i 这一位,那么根据这一位的 0 / 1 0/1 0/1,将其分为两组,根据算法一的分析,最优策略一定是两组内部先连成一个连通块,然后在两组之间各取一个点连起来。

那么每次分治的时候,就将其中一组建出 Trie \text{Trie} Trie 树,另一组每个都放进去查询,然后向两边递归。

时间复杂度 O ( n log ⁡ 2 A ) \mathcal O(n \log^2A) O(nlog2A)

本质上就是算法一从上往下合并。

算法三

众所周知,最小生成树问题用 Prim \text{Prim} Prim Kruskal \text{Kruskal} Kruskal 做不出来的时候,就要考虑用 Boruvka \text{Boruvka} Boruvka

Boruvka \text{Boruvka} Boruvka 是这么一个东西:开始 n n n 个点都是一个连通块,每次遍历所有点和边,连接一个连通块中,和其他连通块相连的最小的一条边,然后把连通块合并起来。这样每次连通块数量至少减少一半,所以时间复杂度是 O ( ( n + m ) log ⁡ n ) \mathcal O\left((n+m)\log n\right) O((n+m)logn)

直接套板子没有出路,但是这个算法有很棒的可优化性,我们只要考虑枚举边的时候怎么优化即可。

考虑每次合并的时候,都对所有点重构一个 Trie \text{Trie} Trie。对于 Trie \text{Trie} Trie 中的每个结点,维护两个值表示该子数中结点所在连通块编号的最小值和最大值,然后枚举点查询的时候,就相当于查询子树内是否存在一个点和这个点的连通块编号不同,用上面的信息即可。

时间复杂度 O ( n log ⁡ n log ⁡ A ) \mathcal O(n\log n\log A) O(nlognlogA)

你可能感兴趣的:(一个最小生成树的经典扩展问题)