关于生成树的个人总结
关于生成树与两个位运算结合的题型总结
对于生成树,我们很容易会想起Kruscal求最小生成树,但是若将边的边权从简单的加减改成位运算,还是有些许难度的。下面总结一下遇到的两种生成树与位运算结合的例题。
1. 求在或运算下 $ ( | ) $ 的最小生成树。
对于位运算,我们会很自然的考虑二进制拆位。我们从高位开始,若当前第 $ i $ 位为 $ 0 $ ,后面的 $ i-1 $ 位无论是多少,都比第 $ i $ 位为 $ 1 $ 要更优。
($ \sum_{i=1}^{N-1} 2^i = 2^N-1 < 2^N$)
所以我们从高位开始向低位贪心,判断当前位是否可以为 $ 0 $ ,可以直接为 $ 0 $ 。
下面考虑如何判断当前位是否可以为0。在之前的限制条件之下,枚举 $ m $ 条边,将所有权值当前位为 $ 0 $ 的边全部选出来,如果这些选出来的边可以将所有点连通起来,那么一定可以找到一颗符合要求的生成树,那么答案的第 $ i $ 位一定为 $ 0 $ ,否则为 $ 1 $ 。
举个例子。现在当前的答案是 $ (00000)_2 $ ,我们已知第四位可以为 $ 0 $ ,当前枚举到第三位,现有两条边的权值的二进制表达式为 $ (10111)_2 $ 和 $ (01000)_2 $ ,首先对于第一条边,我们发现第三位是 $ 0 $ 但是我们发现第四位不为 $ 0 $ ,若加入这条边那么就违反了我们的前提,那么这条边就必须被跳过,无法加入。而对于第二条边,第三位不为 $ 0 $ ,若加入这条边的话,我们无法实现最后的答案第三位为 $ 0 $ ,因此跳过。若当前已经选出所有第三位为 $ 0 $ 的边,并且不违背前提条件。那么我们需要判断这 $ N $ 个点是否连通,若连通,那么答案的这一位为 $ 0 $ ,反之为 $ 1 $ 。关于判断连通,我们用并查集维护一下即可。
原题:
https://codeforces.com/problemset/problem/1624/G
代码:
时间复杂度: $ O_{((n+m)logn)} $
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
int n,m;
const int maxn=2e5+10;
int fa[maxn];
struct EDGE
{
int u,v,w;
bool del;
}e[maxn];
int find(int x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
int fa1=find(x);
int fa2=find(y);
if(fa1==fa2) return ;
fa[fa1]=fa2;
}
bool check(int x,int k)
{
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
if((e[i].w>>k)&1||e[i].del) continue;
merge(e[i].u,e[i].v);
}
for(int i=2;i<=n;i++) if(find(i)!=find(1)) return 0;
for(int i=1;i<=m;i++) if((e[i].w>>k)&1) e[i].del=1;
return 1;
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>e[i].u>>e[i].v>>e[i].w;
e[i].del=0;
}
int ans=0;
for(int i=31;i>=0;i--) if(!check(ans,i)) ans|=(1ll<<i);
cout<<ans<<endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t=1;
cin>>t;
while(t--)
{
solve();
}
return 0;
}
2. 给定 $ n $ 个点的点权,这 $ n $ 个点构成一个无向完全图,两个点之间的边权为这两个点的点权异或和,求这个无向完全图的 $ MST $
注意到异或最值问题,考虑 $ 01trie $ 。我们首先按照字典树从高位向低位建树,如果一个点要与另外一个点(记为 $ A $ 点 和 $ B $ 点)相连 ,那么 $ A $ 点一定要与 $ C $ 点相连,其中$ LCA(A,C)=LCA(A,B) (B 和 C可能重合)$。因为此时,上面的二进制位被异或掉了,否则就会凭空多出贡献。根据分治思想,我们得到:
一个节点下的最小异或生成树=左子树最小异或生成树+右子树最小异或生成树+左右合并产生贡献的最小值
前两个通过递归实现,重点在于第三个:左右子树合并时的最小贡献。
此时问题转换为在两个集合中找到一对数,使得彼此的异或和最小(最小异或对),我们枚举集合较小的元素,去在较大集合中找到与之异或和最小的元素即可(按秩启发式)。
原题:
https://codeforces.com/contest/888/problem/G
代码:
时间复杂度: $ O_{(nlog^2n)} $
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
using i64=long long;
const int maxn=2e5+10;
i64 ans,x;
int n,idx,ch[maxn*31][2];
vector<int>e[maxn*31];
void insert(int x)
{
int p=0;
for(int i=29;~i;i--)
{
int j=x>>i&1;
if(!ch[p][j]) ch[p][j]=++idx;
p=ch[p][j];
e[p].push_back(x);
}
}
int query(int x,int d,int v)
{
if(d<0) return 0;
int now=v>>d&1;
if(ch[x][now]) return query(ch[x][now],d-1,v);
return query(ch[x][now^1],d-1,v)+(1<<d);
}
void work(int p,int d)//分治
{
int ls=ch[p][0];
int rs=ch[p][1];
if(ls&&rs)
{
if(e[ls].size()>e[rs].size()) swap(ls,rs);//启发式合并
int minnum=1<<30;
for(auto it:e[ls]) minnum=min(minnum,query(rs,d-1,it));
ans+=minnum+(1<<d);//如果左右儿子都有,那么要加上这一位的贡献
}
if(ls) work(ls,d-1);
if(rs) work(rs,d-1);
}
void solve()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x;
insert(x);
}
work(0,29);
cout<<ans<<endl;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t=1;
while(t--)
{
solve();
}
return 0;
}
通过此题还了解到:生成树的第三种求法------Boruvka算法。
丢个链接供学习:https://oi-wiki.org/graph/mst/#boruvka-%E7%AE%97%E6%B3%95
浙公网安备 33010602011771号