Codeforces 888G Xor-MST (最小异或生成树)
给定 n 个点,每个点有个点权,任意两点可以连一条边,边权为两点权的异或值,求最小生成树
想法一:暴力求出所有边权,然后把边按边权从小到大排序,用kruskal跑最小生成树
想法二:把边排序后,发现最小的边权就是两个相同的值的异或值(为0),其次就是两个只在第 0 位不同的数的异或值 (为1)....
把01trie画出来,每个叶子节点就代表一个数,可以发现最小权值的边就是一个叶子节点和它本身(两个相同的数),其次就是lca距离叶子节点为一的两个叶子节点(这两个叶子节点只有第0位不同),再其次就是lca距离叶子节点距离为2的两个叶子节点...
于是,从最小的边权开始跑kruskal,肯定是在01trie上lca深度最大的两个叶子节点开始连边,在01tie上画图,可以发现对于每颗子树的根节点,一定是其左右子树上的叶子节点先构成了最小生成树后,再从左子树上选一叶子节点与右子树上选一叶子节点连通,于是就可以递归了
想法三:怎么计算连通左右子树的边的最小权值,可以枚举其中一颗子树上的每一个数,用O(log n)找最小异或数对的方法找与另一颗子树的所有数的异或最小值。其查找次数为O(n log n),所以复杂度为O(n logn logn)
#include<iostream> #include<algorithm> #include<cstdio> using namespace std; const int MAXN = 2e5+7; const int INF = 1e9+7; int a[MAXN]; int n; int cnt = 0; struct NODE{ int ptp[2]; int L,R,size; }trie[MAXN*30]; void add(int x,int id){ int now = 0; for(int i = 30;i>=0;i--){ int t = 0; if(x & (1<<i)) t = 1; int &tt = trie[now].ptp[t]; if(!tt) tt = ++cnt; now = tt; if(!trie[now].L) trie[now].L = id; trie[now].R = id; trie[now].size = trie[now].R - trie[now].L + 1; } } int cal(int s,int pos,int x){//s为根节点,s在pos+1位,找和x取0到pos位时的异或最小值 int now = s; int res = 0; for(int i = pos;i>=0;i--){ int t = 0; if(x & (1<<i)) t = 1; if(trie[now].ptp[t]){ now = trie[now].ptp[t]; } else { now = trie[now].ptp[t^1]; res |= 1<<i; } } return res; } long long solve(int s,int pos){//s在pos位 int x = trie[s].ptp[0],y = trie[s].ptp[1]; if(x && y){//左右子树都存在 int res = INF; if(trie[x].size < trie[y].size){ for(int i = trie[x].L;i <= trie[x].R;i++){//左子树小就枚举左子树 res = min(res,cal(y,pos-2,a[i]) + (1<<pos-1));//x,y在pos-1位 } } else{ for(int i = trie[y].L;i <= trie[y].R;i++){//右子树小就枚举右子树 res = min(res,cal(x,pos-2,a[i]) + (1<<pos-1)); } } return (long long)res + solve(x,pos-1) + solve(y,pos-1); } else if(x){ //只有左子树,递归 return solve(x,pos-1); } else if(y){ return solve(y,pos-1); } return 0; } int main() { cin>>n; for(int i = 1;i <= n;i++) scanf("%d",&a[i]); sort(a+1,a+n+1); for(int i = 1;i <= n;i++) add(a[i],i); long long ans = solve(0,31); printf("%lld\n",ans); return 0; }