关于生成树的个人总结

关于生成树与两个位运算结合的题型总结

对于生成树,我们很容易会想起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

posted on 2025-02-15 18:41  Linear_L  阅读(12)  评论(0)    收藏  举报