有一张 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 n≤105, ai≤109
直接应用 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)。