原题链接:G. Xor-MST
题目大意:
给出一张 n n n 个点的完全图,每个点都有一个权值,点 i i i 和点 j j j 的边权为 a i a_i ai x o r xor xor a j a_j aj,其中 x o r xor xor 代表二进制异或操作。
问你这张图的最小生成树的权值是多少。
解题思路:
因为是完全图,因此边的总数为 n 2 n^2 n2 级别的,本题 n ≤ 2 × 1 0 5 n \leq 2\times 10^{5} n≤2×105 显然不能使用常规的最小生成树求法。
引入 B r o u v k a Brouvka Brouvka 最小生成树的思想:
- 开始时枚举每一个点,对每个点枚举所有它的出边,找到一条价值最小的,且不与其属于同一连通块的边,将所有这样的边标记,若同一连通块内有多条这样的边则取最小值。
- 对所有的边枚举完之后,将所有被标记边相连的两个点相连,合并成一个连通块,可知整张图的连通块数量至少会减半。
- 重复上述步骤,直到图中只有一个连通块时停止,可以保证我们所得的一定是一颗最小生成树。
注意到每次连通块的数量至少会减半,那么算法最多会执行 log n \log n logn 次,复杂度毫无疑问的是 O ( n log n + m log n ) O(n\log n+ m \log n) O(nlogn+mlogn) 的,其中 n n n 为点数, m m m 为边数。
那么问题来了,图中的边是 n 2 n^{2} n2 级别的,我们要去枚举所有边找到一条最小的边,显然不太可能。
注意到边权的计算为 a i a_i ai x o r xor xor a j a_j aj ,我们需要某种方式找到这类两两异或的最小值,显然我们可以用一颗 T r i e Trie Trie 贪心的解决。
那么我们要怎么快速的找到完全图的最小值呢?其实没有那么复杂,用类似主席树的思想即可。
假设你现在有一颗完全记录了 n = 8 n = 8 n=8 个点的 T r i e Trie Trie 树,你现在枚举的点为点 1 1 1,且其所在连通块内有 { 1 , 5 , 6 } \{1,5,6\} {1,5,6},那么你要怎么找到你和其他 { 2 , 3 , 4 , 7 , 8 } \{2,3,4,7,8\} {2,3,4,7,8} 的点,边权的最小值呢?
我们用大 T r i e Trie Trie 树减去我们当前 { 1 , 5 , 6 } \{1,5,6\} {1,5,6} 这个连通块的小 T r i e Trie Trie 树不就相当于从 n = 8 n=8 n=8 个点中将 { 1 , 5 , 6 } \{1,5,6\} {1,5,6} 这三个点挖去了吗?
T r i e { 2 , 3 , 4 , 7 , 8 } = T r i e { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } − T r i e { 1 , 5 , 6 } \begin{align} Trie_{\{2,3,4,7,8\}} =& Trie_{\{1,2,3,4,5,6,7,8\}} - Trie_{\{1,5,6\}} \end{align} Trie{2,3,4,7,8}=Trie{1,2,3,4,5,6,7,8}−Trie{1,5,6}
(这一步和主席树做差的思想一致,通过 [ 1 , r ] [1, r] [1,r] 的前缀信息与 [ 1 , l − 1 ] [1,l - 1] [1,l−1] 的前缀信息相减即可得 [ l , r ] [l, r] [l,r] 的信息)
得到做差后的树后,我们就可将 a 1 a_1 a1 扔进这颗做差得到的 T r i e Trie Trie 贪心查询 x o r xor xor 最小值就可得边权最小的边了。
查询完之后,我们就获得了若干个信息,即 x x x 所在连通块与 y y y 所在连通块相连的边权最小,此时我们只需要将 x x x 和 y y y 的 T r i e Trie Trie 做一个类似线段树合并即可。
假设 a i a_i ai 值域为 [ 0 , V ] [0,V] [0,V] ,那么我们单次查询的复杂度为 O ( log V ) O(\log V) O(logV),总复杂度为 O ( n log V ) O(n \log V) O(nlogV), B r o u v k a Brouvka Brouvka 最多会执行 O ( log n ) O(\log n) O(logn) 次, T r i e Trie Trie 合并均摊复杂度为 O ( n log V ) O(n \log V) O(nlogV)。
因此复杂度为 O ( n log n log V ) O(n \log n \log V) O(nlognlogV) ,可以通过。具体细节参考代码即可。
#include <bits/stdc++.h>
using namespace std;
using PII = pair<int, int>;
using i64 = long long;
const int N = 5e5 + 1, logn = 30;
//T为 Trie 树,cnt为该节点数量信息(cnt[x] - cnt[y] 即 x树 - y树的信息)
//leaf用于记录 ai 的下标,有多个相同的 ai 任取一个即可
int T[N * logn][2], cnt[N * logn], leaf[N * logn], idx;
//普通的插入初始化函数
void insert(int& R, int val, int id) {
if (!R) R = ++idx;
int cur = R, p = 0;
for (int j = logn; j >= 0; --j) {
p = val >> j & 1;
++cnt[cur];
if (!T[cur][p]) {
T[cur][p] = ++idx;
}
cur = T[cur][p];
}
++cnt[cur];
leaf[cur] = id;
}
//贪心查询最小值函数(返回最小值 aj 的下下标 j)
int Query(int pre, int cur, int val) {
int p = 0;
for (int j = logn; j >= 0; --j) {
p = val >> j & 1;
if (cnt[T[cur][p]] - cnt[T[pre][p]]) {
cur = T[cur][p];
pre = T[pre][p];
} else {
cur = T[cur][p ^ 1];
pre = T[pre][p ^ 1];
}
}
return leaf[cur];
}
//Trie 合并
int Merge(int pre, int cur) {
if (!pre || !cur) {
return pre | cur;
}
cnt[cur] += cnt[pre];
T[cur][0] = Merge(T[pre][0], T[cur][0]);
T[cur][1] = Merge(T[pre][1], T[cur][1]);
return cur;
}
//并查集板子 注意merge函数
struct DSU {
//ver[i] 代表点 i 所在并查集的 Trie树(注意 i 必须为并查集的根节点)
vector<int> fa, siz, ver;
DSU(int n) : ver(n + 1), fa(n + 1), siz(n + 1, 1) {
iota(fa.begin(), fa.end(), 0);
}
int find(int x) {
while (x != fa[x]) {
x = fa[x] = fa[fa[x]];
}
return x;
}
int size(int x) { return siz[find(x)]; }
bool same(int x, int y) { return find(x) == find(y); }
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return false;
if (siz[x] > siz[y]) {
swap(x, y);
}
//将 x -> y 的连通块合并时 同时将 x树 合并到 y树内
ver[y] = Merge(ver[x], ver[y]);
siz[y] += siz[x];
fa[x] = y;
return true;
}
};
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
//初始化并查集与 Trie
DSU dsu(n);
sort(a.begin(), a.end());
for (int i = 1; i <= n; ++i) {
insert(dsu.ver[0], a[i], i);
insert(dsu.ver[i], a[i], i);
}
i64 ans = 0;
//Brouvka迭代到并查集节点数为 n 即可停止
while (dsu.size(1) != n) {
//Edge[u]表示点u所在连通块最小边的信息,即 [a[u] ^ a[j]的边权,j的编号]
vector<PII> Edge(n + 1, PII(INT32_MAX, 0));
//枚举每个点 i 找最小值
for (int i = 1; i <= n; ++i) {
int u = dsu.find(i);//获得根节点的Trie树
int v = Query(dsu.ver[u], dsu.ver[0], a[i]);//用大树和u树做差的同时查询最小值
Edge[u] = min(Edge[u], PII(a[i] ^ a[v], v));//多条边中取最小即可
}
//将所有连通块合并
for (int u = 1; u <= n; ++u) {
if (dsu.find(u) != u) {
continue;
}
auto& [d, v] = Edge[u];
if (dsu.merge(u, v)) {
ans += d;//加上权值
}
}
}
cout << ans << "\n";
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1; //cin >> t;
while (t--) solve();
return 0;
}