CF888G Xor-MST(01trie,MST)
前沿
刚学习了 Borůvka 生成树算法,去写几道题,结果这道题用的是 01Trie,可能是用到了这个算法的思想吧。
题目大意
\(n\) 个点的无向完全图,每条边的权值定义为它两个端点的异或值,求这个图的最小生成树。
题目解析
生成树算法一般有 Kruskal,Prim 和 Borůvka 算法,一般对于边权是由点权0转变过来的求 MST 的题目,一般采用 Borůvka 求解。
Borůvka 是一种维护联通块,每轮连多条边的算法,因为最多只会进行 \(log\) 轮,所以复杂度是 \(mlogn\) 的,当我们用一些数据结构维护后,复杂度可以降低,从而达到通过本题的复杂度。
当然一些题只是用的此算法的思想,并没有真正的在代码上写出此算法。
回到本题,我们显然可以建一棵 01Trie 出来。
我们发现如果两个节点共同的部分(即 01trie 上的lca)深度越深,显然它们的异或值越小,所以我们可以利用贪心的思想,即在trie树上贪心。
对于当前的一个节点,如果它的左右儿子都存在,那么显然左子树和右子树一定会在此节点去合并成一个集合,那么我们可以去找到左子树和右子树中的哪两个值异或的值最小,但是这样显然复杂度是不对劲的。
所以我们可以采用启发式合并的思想,每次把小的去往大的里面找,这样就可以做到正常复杂度,复杂度大概是 \(O(nlog^2n)\)。
在合并两棵子树时, 我们把 size 较小的那棵树的所有节点拿出来, 然后在另一棵子树上贪心地走, 最后对较小子树的所有节点的结果取一个最小值就好了。
相当于一棵 01Trie 在 另一棵 01Trie 上去匹配。
题目代码
// by longdie
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 5;
ll Min;
int n, tot, ch[N*20][2], siz[N*20];
void ins(int x) {
int now = 0; siz[now]++;
for(register int i = 29; i >= 0; --i) {
int c = (x >> i & 1);
if(!ch[now][c]) ch[now][c] = ++tot;
now = ch[now][c], siz[now]++;
}
}
void merge(int u, int v, int dep, ll sum) {
if(sum >= Min) return;
if(dep < 0) { Min = min(Min, sum); return; }
if(ch[u][0]) {
if(ch[v][0]) merge(ch[u][0], ch[v][0], dep-1, sum);
else if(ch[v][1]) merge(ch[u][0], ch[v][1], dep-1, sum+(1<<dep));
}
if(ch[u][1]) {
if(ch[v][1]) merge(ch[u][1], ch[v][1], dep-1, sum);
else if(ch[v][0]) merge(ch[u][1], ch[v][0], dep-1, sum+(1<<dep));
}
}
ll query(int u, int dep) {
if(dep < 0) return 0;
ll res = 0;
if(ch[u][0]) res += query(ch[u][0], dep - 1);
if(ch[u][1]) res += query(ch[u][1], dep - 1);
if(ch[u][0] && ch[u][1]) {
res += 1 << dep, Min = 0x3f3f3f3f3f3f3f3f;
if(siz[ch[u][0]] <= siz[ch[u][1]]) merge(ch[u][0], ch[u][1], dep - 1, 0);
else merge(ch[u][1], ch[u][0], dep - 1, 0);
res += Min;
}
return res;
}
signed main() {
scanf("%d", &n);
for(register int i = 1; i <= n; ++i) {
int x; scanf("%d", &x), ins(x);
}
printf("%lld\n", query(0, 29));
return 0;
}