[解题报告] CF888G Xor-MST (Trie 树 + 异或)
题意
给定一个 \(n\) 个节点的无向完全图, 点 \(i\) 的权值为 \(a_i\), 边 \((i,j)\) 的权值为 \(a_i \oplus a_j\).
求该图的最小生成树的边权之和.
\(1 \le n \le 2 \times 10^5,\ 0 \le a_i < 2^{30}\).
思路
开始根据 Prim 算法想了一个奇奇怪怪的线段树, 写了一个多小时发现是假的......
然后就滚去看题解了.
求最小生成树的过程中, 我们要使得合并的两个点的异或值尽量小, 那么就要求两个数按二进制位从大到小相同的位数尽量的多, 我们可以考虑按 \(a_i\) 的二进制位建一棵 Trie 树, 然后在 Trie 树上贪心.
显然, 在 Tire 树上, 一定是按深度从大到小合并两棵子树最优 (因为这样它们的前面几位异或起来都为 0).
在合并两棵子树时, 我们把 size 较小的那棵树的所有节点拿出来, 然后在另一棵子树上贪心地走, 最后对较小子树的所有节点的结果取一个最小值就好了.
考虑一下时间复杂度. 因为每次我们都是将较小的那棵子树拿出来, 所以每一层最多拿出来 \(\frac{n}{2}\) 个点 (当 Trie树是完全二叉树时). 总共有 \(\log a\) 层, 每个点贪心的复杂度是 \(O(\log a)\) 的, 所以总时间复杂度为 \(O(n \log^2 a)\).
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int _ = 1e5 + 7;
const int __ = 5e6 + 7;
const int L = 29;
struct tire {
int ls[__], rs[__], sz[__], rt, cnt, minx;
void insert(int &k, int t, int x) {
if (!k) k = ++cnt;
++sz[k];
if (t < 0) return;
if (x >> t & 1) insert(rs[k], t - 1, x);
else insert(ls[k], t - 1, x);
}
void ins(int x) { insert(rt, L, x); }
void merge(int k1, int k2, int t, int sum) {
if (sum > minx) return;
if (t < 0) { minx = min(minx, sum); return; }
if (ls[k1]) {
if (ls[k2]) merge(ls[k1], ls[k2], t - 1, sum);
else merge(ls[k1], rs[k2], t - 1, sum + (1 << t));
}
if (rs[k1]) {
if (rs[k2]) merge(rs[k1], rs[k2], t - 1, sum);
else merge(rs[k1], ls[k2], t - 1, sum + (1 << t));
}
}
ll qu(int k, int t) {
if (!k || t < 0) return 0;
ll t1 = qu(ls[k], t - 1);
ll t2 = qu(rs[k], t - 1);
if (ls[k] * rs[k] == 0) return t1 + t2;
else {
minx = 0x3f3f3f3f;
if (sz[ls[k]] < sz[rs[k]]) merge(ls[k], rs[k], t - 1, 0);
else merge(rs[k], ls[k], t - 1, 0);
return t1 + t2 + minx + (1 << t);
}
}
ll query() { return qu(rt, L); }
}T;
int n, a;
int main () {
#ifndef ONLINE_JUDGE
freopen("x.in", "r", stdin);
#endif
cin >> n;
for (int i = 1; i <= n; i++) {
scanf("%d", &a);
T.ins(a);
}
printf("%lld\n", T.query());
return 0;
}