题目传送门
题目大意: 有 n n n 个点,每个点有一个权值,两个点之间边的长度为两点权值的异或值,求最小生成树的边权和。
把 Boruvka \text{Boruvka} Boruvka 算法拿来用,计算两个连通块的最短边时,分别维护出两块的 01 trie 01~\text{trie} 01 trie,遍历其中一个就可以算出最短边,具体来说就是贪心,从最高位开始使每一位尽可能相同。然后合并两块时,将他们的 trie \text{trie} trie 也合并起来,那么最后就会得到一棵包含所有数的 trie \text{trie} trie。
这种方法的问题在于,没办法每次都求出两两间最短边的长度,进一步的,就没法求出一个连通块连出去的最短边,但是提供了一个很好的思路,即只需要确定连通块的合并顺序,就容易求出答案了。
考虑包含所有数的那颗 trie \text{trie} trie,他的一个节点的子树就对应一个点集,容易发现,从下往上对于每个点合并他的左右子树,这种策略是最优的合并顺序,因为这样可以保证,每个连通块在合并时,一定是沿着最短的边与别人合并的。
于是每次只需要求左右子树的合并最短边即可,大力把左子树内的每个点丢到右子树内跑贪心即可,时间复杂度 O ( n log 2 n ) O(n\log^2n) O(nlog2n)。
代码如下:
#include
#include
#include
using namespace std;
#define maxn 200010
#define inf 999999999
#define ll long long
int n,a[maxn];
struct node{
int l,r,dep;
node *ch[2];
node(int Dep):l(inf),r(0),dep(Dep){
ch[0]=ch[1]=NULL;}
}*root;
void insert(int x){
node *now=root;
for(int i=29;i>=0;i--){
int c=a[x]>>i&1;
if(!now->ch[c])now->ch[c]=new node(i-1);
now=now->ch[c];
now->l=min(now->l,x);
now->r=max(now->r,x);
}
}
int ask(node *now,int x){
if(now->dep<0)return 0;
int c=x>>(now->dep)&1;
if(now->ch[c])return ask(now->ch[c],x);
else return ask(now->ch[c^1],x)+(1<<(now->dep));
}
ll solve(node *now){
if(now->ch[0]&&now->ch[1]){
int mi=inf;
for(int i=now->ch[0]->l;i<=now->ch[0]->r;i++)mi=min(mi,ask(now->ch[1],a[i]));
return solve(now->ch[0])+solve(now->ch[1])+mi+(1<<now->dep);
}
if(now->ch[0])return solve(now->ch[0]);
if(now->ch[1])return solve(now->ch[1]);
return 0;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
sort(a+1,a+n+1);root=new node(29);
for(int i=1;i<=n;i++)insert(i);
printf("%lld",solve(root));
}