题目描述
给定 \(n\) 个点的无向完全图。每个点的点权为 \(a_i\),连接点 \(i\) 和点 \(j\) 的边的边权为 \(a_i\oplus a_j\),求这个图的最小生成树的权值\((1\leq n\leq 2\times 10^5,0\leq a_i< 2^{30})\)。
分析
由于不可能预处理出所有边的边权,而且边权与异或有关,所以把样例一从高位到低位插入字典树中观察:
可以发现对于每个叶子节点(即点权)之间,如果需要得到相互连边后的边权,求它们的 $\text{LCA} $ 之后的异或值即可。
因此如果两点间的 $\text{LCA} $ 深度越大,边权值便越小。因为数字从高位到低位插入字典树,所以深度越小的 $\text{LCA} $ 连边后的边权越大,尽量避免选 $\text{LCA} $ 小的点。
可以发现恰好有 \(4\) 个 \(\text{LCA }\),也就是有两个儿子的节点一共有 \(4\) 个,如果 \(a_i\) 两两不等,那么这样的点一共有 \(n-1\) 个,对应最小生成树的 \(n-1\) 条边。
每找到有两个儿子的点,就贪心 \(dfs\) 下去:
\(1.\) 每次都尽量同时走左儿子/右儿子。
\(2.\) 如果左右儿子都有,就两个儿子都走,返回值取 \(\min\)。
\(3.\) 如果两个点只有不同的儿子,就在返回值上加上 \(2^{bit}\),然后继续走。
最后的答案就是 \(dfs\) 的和。
如果 \(a_i\) 之间不是两两不等,则直接连接点权相同的 \(a_i\),边权为 \(0\),对答案没有影响。
根据启发式合并的思想,每次连边的时间复杂度等于 树的深度 \(\times\) 小的子树大小,所以 \(dfs\) 与连边的时间复杂度不会超过 \(O(n\log^2 n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int trie[N*30][2],a[N],tot=0;
void insert(int x)
{
int p=0;
for(int i=30;i>=0;i--)
{
int ch=(x>>i)&1;
if(!trie[p][ch])
trie[p][ch]=++tot;
p=trie[p][ch];
}
}
int solve(int root1,int root2,int bit)
{
if(bit<0)
return 0;
int ans1=-1,ans2=-1;
if(trie[root1][0]&&trie[root2][0])
ans1=solve(trie[root1][0],trie[root2][0],bit-1);
if(trie[root1][1]&&trie[root2][1])
ans2=solve(trie[root1][1],trie[root2][1],bit-1);
if(ans1>=0&&ans2>=0)
return min(ans1,ans2);
if(ans1>=0)
return ans1;
if(ans2>=0)
return ans2;
if(trie[root1][0]&&trie[root2][1])
ans1=solve(trie[root1][0],trie[root2][1],bit-1)+(1<<bit);
if(trie[root1][1]&&trie[root2][0])
ans2=solve(trie[root1][1],trie[root2][0],bit-1)+(1<<bit);
if(ans1>=0&&ans2>=0)
return min(ans1,ans2);
if(ans1>=0)
return ans1;
if(ans2>=0)
return ans2;
}
long long ans=0;
void dfs(int start,int bit)
{
if(bit<0)
return ;
if(trie[start][0]&&trie[start][1])
ans=ans+1ll*solve(trie[start][0],trie[start][1],bit-1)+(1<<bit);
if(trie[start][0])
dfs(trie[start][0],bit-1);
if(trie[start][1])
dfs(trie[start][1],bit-1);
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
insert(a[i]);
}
dfs(0,30);
cout<<ans<<endl;
return 0;
}