*异或* 最小 ? 树

本文介绍了如何通过Boruvka算法和分治技巧解决异或最小生成树问题,以及如何运用类似01Trie的思路在ClubAssignment和XorTree中寻找最大或最小异或值。作者分享了从CF888G到澳门2020 C题的解题过程,强调了异或问题中的单调性和构造策略的重要性。
摘要由CSDN通过智能技术生成

前几天VP了2020澳门赛区,被一道异或题摆了一道,因此今天找来几道异或的题目做一做,顺便总结一下。
遇到异或的题目,如果性质很强而且每一位有显然相互独立互不影响的特点,我们常采用按位考虑的方法。但是遇到和这些好不相干的题目时,我们常使用01Trie(+分治)或者线性基的方法来处理。本文将浅谈一下前一种做法。

首先是一道板子题,异或最小生成树

CF888G. Xor-MST

给定一个n个点组成的完全图,给定他们的点权,任意两个点之间的边权是二者之间的点权异或值,求完全图的最小异或生成树。
通过思考很容易排除Kruskal算法和Prim算法的可行性,因此我们需要使用另外一种最小生成树的方法:完全图最小生成树—Boruvka 算法,算法核心就是初始时每个点自己形成一个连通块,然后进行不停的迭代,每一次迭代的过程中每个连通块找除了这个连通块内的点之外的所有点中的最小异或值,并向那个点连边,直到所有点构成一个连通块。由于每一次迭代连通块都在减半,因此迭代次数至多 \log{n} 轮。实现方法就是用一棵Trie树,然后每一次遍历所有连通块把连通块内的点删除之后再找出最小的边对应的点进行连通。算法时间复杂度为 O(n\log^{2}{n})
当然这道题借助Boruvka算法的思想,我们可以建立一颗Trie树,然后从高位开始考虑,由于二进制下只有0,1两种取值,天然的分成了两个待连通的集合,我们贪心的想会发现0和0,1和1在一起一定最好,然后我们需要连通这两个集合,这里我们可以使用刚刚Boruvka的过程,删除一个集合内的所有的点,然后遍历这个集合内的所有的点来找到权值最小的联通到另外一个集合的边;but似乎这样过于暴力,因此我们可以想到加一个启发式,每次对数量小的集合进行操作,这样找边的复杂度就能够保证为均摊 O(\log{n}) ,然后再在两个集合内进行递归下一位的连通。
/完全图最小生成树—Boruvka O(nlognlogn)/

#define N 200010
int n,m,k,a[N];
struct Trie{
	int tr[N*30][2],idx,cnt[N*30],id[N*30];
	void insert(int x){
		int p=0;
		for(int i=29;i>=0;--i){
			int u=x>>i&1;
			if(!tr[p][u]) tr[p][u]=++idx;
			p=tr[p][u]; cnt[p]++;
		}
	}
	void remove(int x){
		int p=0;
		for(int i=29;i>=0;--i){
			int u=x>>i&1;
			p=tr[p][u];
			cnt[p]--;
		}
	}
	int query(int x){
		int p=0,ans=0;
		for(int i=29;i>=0;--i){
			int u=x>>i&1;
			if(tr[p][u]&&cnt[tr[p][u]]>0) p=tr[p][u];
			else p=tr[p][u^1],ans|=1<<i;
		}
		return ans;
	}
	LL dfs(vector<int>&v, int p, int k){
		if(v.size()<2||(!tr[p][0]&&!tr[p][1])) return 0;
		if(!tr[p][0]) return dfs(v,tr[p][1], k-1);
		if(!tr[p][1]) return dfs(v,tr[p][0], k-1);
		
		vector<int> left(0),right(0);
		for(auto u:v){
			if(u>>k&1) right.push_back(u);
			else left.push_back(u);
		} 
		if(right.size()>=left.size()){
			for(auto u:left) remove(u);
			int res=1e9;
			for(auto u:left)  res=min(res,query(u));
			for(auto u:left) insert(u);
			return res+dfs(left,tr[p][0],k-1)+dfs(right,tr[p][1],k-1);
		}
		else{
			for(auto u:right) remove(u);
			int res=1e9;
			for(auto u:right)  res=min(res,query(u));
			for(auto u:right) insert(u);
			return res+dfs(left,tr[p][0],k-1)+dfs(right,tr[p][1],k-1);
		}
	}
}T;
vector<int> v;
void solve(){
	n=read();
	rep(i,1,n) a[i]=read(),v.push_back(a[i]);
	for(int i=1;i<=n;++i) T.insert(a[i]);
	LL ans=T.dfs(v,0,29);
	print(ans);
}

然后是2020年澳门的这道C
一开始看到最小值最大就直接冲二分+Trie了,结果十分钟写完WA了1发后意识到自己是个sha*,但是很快另外一道构造题有了思路就去写那道题了,后来这道题赛时也没出了…回来补的时候还是想了一会儿的。

C. Club Assignment
意思是n个点,给定点权,要求将n个点划分到两个集合内,是的两个集合内的异或最小值中的最小值最大。
这道题和NOIP2015关押罪犯非常的相似,相似到了令人发指的程度,也为最大值最小/最小值最大提供了两外一种思路。
关押罪犯那道题求得是最大值最小,因此就从最大边开始构造起,来构造出矛盾,矛盾值即为最大值的最小值。本题同理,构造最大的最小值,并且二进制异或天然划分出了0和1两个集合,因此我们从最高位开始构造最小值,如果能构造出来就继续下一位构造,否则如果出现矛盾,当前值就为最小值得最大值。
究其根本原因:
证明一:这个异或值的最小值关于划分方式存在单调性。因此实际上我们在构造一棵异或最小生层树,当可以构造出来的时候说明不论则么构造都会出现最小异或值为0的情况。否则,存在一种划分方式使得异或最小值最大。
另一种:尝试反向判断,考虑存在合法方案时,补图的相关限制。由于“完全图”的限制,这些实际不存在的边的顶点必须分属两个集合,因此补图为二分图为必要条件。尝试证明充分性,若补图为二分图,则不存在 同一集合的两点间没有连边 的情况,则两集合一定均为完全图。故补图为二分图为充要条件,证毕。[1]
构造方法:采用分治法:从高位向低位考虑,按照0,1划分成两个块,这样子能够保证我们能够对当前位构造出最小异或值。如果当前0块或者1块中的数量大于3就说明不论怎么构造,总能够构造出当前位在两个集合中异或值为0,则继续对两个块内进行分治。如果两个块内的数量都小于2,说明对于当前位存在构造方式使得构造出来的两个集合内的异或值的最小值不为0,贪心思路下我们应该在这一位中构造最大值。构造方法就是暴力枚举两个块的所有配对方式,找到所有配对方式中最小的对<i,j>,我们将这两个分别放入两个集合,就能够造出异或最小值的最大值了。
复杂度分析:分治递归最多 \log{n} 层,每一层遍历都会线性遍历整个集合,因此总复杂度为 O(n\log{n})

#define N 200010
#define INF 1e9
map<int,int> mp;
int res[N];
int n,a[N];

LL dfs(vector<pii> &v,int k){
	if(k==-1){
		for(auto u:v) res[u.y]=1;
		return 0;
	}
	if(v.size()<=2) {if(v.size())res[v.begin()->y]=1,res[v.rbegin()->y]=2;return INF;}
	vector<pii> left(0),right(0);
	for(auto u:v) {
		if(u.x>>k&1) right.push_back(u);
		else left.push_back(u);
	}
	if(!left.size()||!right.size()) 
		return min(dfs(left, k-1), dfs(right, k-1));
	if(left.size()<=2&&right.size()<=2){ //暴力枚举分组,得到异或最大值
		int ans=INF,x=0,y=0,tmp=INF;
		for(int i=0;i<left.size();++i){
			for(int j=0;j<right.size();++j){
				if((left[i].x^right[j].x)<tmp) tmp=(left[i].x^right[j].x),x=i,y=j;
			}
		}
		res[left[x].y]=1,res[right[y].y]=2;
		if(left.size()>1) {
			res[left[!x].y]=2,ans=min(ans, left[!x].x^right[y].x);
		}
		if(right.size()>1){
			res[right[!y].y]=1,ans=min(ans, right[!y].x^left[x].x);
		}
		return ans;
	}
	return min(dfs(left, k-1), dfs(right, k-1));
}

void solve(){
	bool ok=true;mp.clear();
	n=read();
	rep(i,1,n) {
		a[i]=read(),mp[a[i]]++;
		if(mp[a[i]]>=3) ok=false;
	}
	if(!ok) {
		print(0);
		for(int i=1;i<=n;++i) printf("1");puts("");
		return ;
	}
	vector<pii> v(0);
	for(int i=1;i<=n;++i) v.push_back({a[i],i});
	LL ans=dfs(v, 29);
	print(ans);
	for(int i=1;i<=n;++i) printf("%d",res[i]);
	puts("");
}

后来也看了一些题解,都说和01Trie有关,什么构造完最小异或生成树只会找最小值。。。其实没太看明白。因为根本这道题没有用到01Trie.但是想起来之前一个算法群里的大神说过,反应过来其实这个分治递归的过程相当于是建立起了一棵01Trie…妙啊。

然后是最近VP的一道题目:一道Trie+分治的好题。
E. Xor Tree
大意是n个点给定点权,每个点都会向除了该点的异或最小值连边,当存在i连向j,j连向i的时候我们当成只练了一条边。问最少删除多少个点使得此连边方式会形成一棵树。
这题也非常有意思,因为如果能够构成一棵树,一定是存在且仅存在一条互为最小异或值的点对 。那么从高位开始考虑,对于0,1两个集合,每个集合内都有可能存在互为最小异或值的点对,当且仅当这个集合内只有1个点的时候满足只存在一对。那么贪心的思路就出来每次对于两个集合,数量较小的集合我们只保留一个,然后对数量更多的集合进行分治。至于每一位的数量可以通过字典树来进行维护。

#define N 200010
int n,m,k,a[N];
struct Trie{
	int tr[N*30][2],idx,cnt[N*30];
	void insert(int x){
		int p=0;
		for(int i=29;i>=0;--i) {
			int u=x>>i&1;
			if(!tr[p][u]) tr[p][u]=++idx;
			p=tr[p][u];
			cnt[p]++;
		}
	}
	int query(int p){
		if(!tr[p][0]&&!tr[p][1]) return 1;
		if(!tr[p][0]) return query(tr[p][1]);
		if(!tr[p][1]) return query(tr[p][0]);
		return max(query(tr[p][0]), query(tr[p][1]))+1;
	}
}T;

void solve(){
	n=read();
	rep(i,1,n) a[i]=read();
	rep(i,1,n) T.insert(a[i]);
	print(n-T.query(0));
}

经过这么几题的思考和之前做题的一些积累,我对树上异或问题和异或最小问题有了更深的体会。

[1] 引用自题解 P1525 【关押罪犯】 - littlewyy 的博客 - 洛谷博客 littlewyy在关押罪犯中的证明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值