CF888G 题解
前置知识 - Borůvka 算法
该算法用于求解最小生成树。
定义一次 “Borůvka 操作” 如下:
枚举所有不在连通块内的边。若一条边连接两个不同的连通块 \(x,y\),且这条边是连接 \(x,y\) 的边中权值最小的,那么就用这条边连接 \(x,y\),把 \(x,y\) 合并成一个连通块。
重复上述操直到所有点都在同一个连通块内为止。这样我们就得到了一棵最小生成树。
注意到我们每次使用 \(O(m)\) 的代价使一个规模为 \((n,m)\) 的问题变为了一个规模为 \((\frac{n}{2},m)\) 的问题。所以时间复杂度为 \(O(m\log n)\)。
对于存在相同边权的图。比较两条边时需要多加一个关键字,通常为边的编号,这是为了防止两个连通块互相连的时候出现环。
给个代码(模板题):
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define gt getchar
#define pt putchar
#define fst first
#define scd second
typedef long long ll;
const int N=5005;
const int M=2e5+5;
using namespace std;
using namespace __gnu_pbds;
typedef pair<int,int> pii;
inline bool __(char ch){return ch>=48&&ch<=57;}
template<class T> inline void read(T &x){
x=0;bool sgn=0;char ch=gt();
while(!__(ch)&&ch!=EOF) sgn|=(ch=='-'),ch=gt();
while(__(ch)) x=(x<<1)+(x<<3)+(ch&15),ch=gt();
if(sgn) x=-x;
}
template<class T,class ...T1> inline void read(T &x,T1 &...x1){
read(x);
read(x1...);
}
template<class T> inline void print(T x){
static char st[70];short top=0;
if(x<0) pt('-');
do{st[++top]=x>=0?(x%10+48):(-(x%10)+48),x/=10;}while(x);
while(top) pt(st[top--]);
}
template<class T> inline void printsp(T x){
print(x);
putchar(' ');
}
template<class T> inline void println(T x){
print(x);
putchar('\n');
}
int n,m,u[M],v[M],w[M],best[N];
int fa[N],siz[N];
bool used[M];
int find(int x){
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
inline bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return 0;
if(siz[x]<siz[y]) swap(x,y);
fa[y]=x,siz[x]+=siz[y];
return 1;
}
inline bool better(int x,int y){
if(!y) return 1;
if(w[x]!=w[y]) return w[x]<w[y];
return x<y;
}
inline void Boruvka(){
for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;
int cnt_edge=0; ll sum=0;
bool flag=1;
while(flag){
flag=0;
for(int i=1;i<=n;++i) best[i]=0;
for(int i=1;i<=m;++i){
if(used[i]) continue;
int x=find(u[i]),y=find(v[i]);
if(x==y) continue;
if(better(i,best[x])) best[x]=i;
if(better(i,best[y])) best[y]=i;
}
for(int i=1;i<=n;++i){
if(best[i]&&!used[best[i]]){
flag=1,cnt_edge++;
sum+=w[best[i]];
used[best[i]]=1;
merge(u[best[i]],v[best[i]]);
}
}
}
if(cnt_edge!=n-1) printf("orz\n");
else println(sum);
}
signed main(){
read(n,m);
for(int i=1;i<=m;++i) read(u[i],v[i],w[i]);
Boruvka();
return 0;
}
正题
看到异或,想到 01trie。
首先对 \(a\) 数组去重,显然只考虑这些点构成的图的 MST 与原图是等价的。
对全局建一棵 01trie,再对每一个点都建一棵 trie,动态开点。
由于我们肯定不能把边连出来,所以要稍微修改一下 Borůvka 算法的流程:
-
修改 \(best\) 的定义:\(best_i\) 表示 \(i\) 这个点连到其他联通块最短边的另一个端点。并定义 \(mn_i\) 表示这条边的边权。初始为 \(+\infty\)。
-
枚举每个点 \(i\),用全局的 trie 减去 \(i\) 的 trie,然后在上面找到和另一个点 \(j\) 的最小异或和以及 \(j\) 的编号。如果 \(i,j\) 不在同一连通块里,用这条边更新 \(best\) 和 \(mn\)。
-
再次枚举所有点,如果 \(mn_i\not=+\infty\) 且 \(i\) 与 \(best_i\) 不在同一连通块内(这是防止合并两次),生成树大小加上 \(mn_i\),并在并查集 & 01trie 中合并 \((i,best_i)\)(类似线段树合并)。
时间复杂度 \(O(n\log n\log V)\),空间复杂度 \(O(n\log V)\)。
代码:
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define gt getchar
#define pt putchar
#define fst first
#define scd second
typedef long long ll;
const int N=2e5+5;
using namespace std;
using namespace __gnu_pbds;
typedef pair<int,int> pii;
inline bool __(char ch){return ch>=48&&ch<=57;}
template<class T> inline void read(T &x){
x=0;bool sgn=0;char ch=gt();
while(!__(ch)&&ch!=EOF) sgn|=(ch=='-'),ch=gt();
while(__(ch)) x=(x<<1)+(x<<3)+(ch&15),ch=gt();
if(sgn) x=-x;
}
template<class T,class ...T1> inline void read(T &x,T1 &...x1){
read(x);
read(x1...);
}
template<class T> inline void print(T x){
static char st[70];short top=0;
if(x<0) pt('-');
do{st[++top]=x>=0?(x%10+48):(-(x%10)+48),x/=10;}while(x);
while(top) pt(st[top--]);
}
template<class T> inline void printsp(T x){
print(x);
putchar(' ');
}
template<class T> inline void println(T x){
print(x);
putchar('\n');
}
int n,a[N];
namespace Trie{
int tot,ch[N*50][2],rt[N*50],siz[N*50],tail[N*50];
void insert(int &p,int dep,int pos,int w){
if(!p) p=++tot;
if(dep<0) return tail[p]=pos,siz[p]=1,void();
int bit=(w>>dep)&1;
insert(ch[p][bit],dep-1,pos,w);;
siz[p]++;
}
void merge(int &L,int R){
if(!L||!R) return L=L+R,void();
merge(ch[L][0],ch[R][0]);
merge(ch[L][1],ch[R][1]);
siz[L]=siz[ch[L][0]]+siz[ch[L][1]];
tail[L]=tail[R];
}
inline pii query(int pre,int p,int x){
int ans=0;
for(int i=30;i>=0;--i){
int bit=(x>>i)&1;
if(ch[p][bit]&&(siz[ch[p][bit]]-siz[ch[pre][bit]]>0)) p=ch[p][bit],pre=ch[pre][bit];
else ans+=(1<<i),p=ch[p][!bit],pre=ch[pre][!bit];
}
return {ans,tail[p]};
}
}
int mn[N],best[N];
namespace DSU{
int fa[N],siz[N];
inline void init(){
for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;
}
int find(int x){
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
inline bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return 0;
if(siz[x]<siz[y]) swap(x,y);
fa[y]=x,siz[x]+=siz[y];
Trie::merge(Trie::rt[x],Trie::rt[y]);
return 1;
}
}
inline void Boruvka(){
DSU::init();
ll sum=0;
bool flag=1;
while(flag){
flag=0;
memset(mn,0x3f,sizeof(mn));
for(int i=1;i<=n;++i){
int x=DSU::find(i);
pii res=Trie::query(Trie::rt[x],Trie::rt[0],a[i]);
int y=DSU::find(res.scd),w=res.fst;
if(x==y) continue;
if(w<mn[x]) mn[x]=w,best[x]=y;
if(w<mn[y]) mn[y]=w,best[y]=x;
}
for(int i=1;i<=n;++i){
if(mn[i]!=0x3f3f3f3f&&DSU::find(i)!=DSU::find(best[i])){
flag=1,sum+=mn[i];
DSU::merge(i,best[i]);
}
}
}
println(sum);
}
signed main(){
read(n);
for(int i=1;i<=n;++i) read(a[i]);
sort(a+1,a+n+1);
n=unique(a+1,a+n+1)-(a+1);
for(int i=1;i<=n;++i){
Trie::insert(Trie::rt[0],30,i,a[i]);
Trie::insert(Trie::rt[i],30,i,a[i]);
}
Boruvka();
return 0;
}