并查集及其优化和练习

问题引入

  • 我们常常会遇到这样的问题:
    有一群人,他们有一些人是认识的,假设A认识B,B认识C,那么称A,B,C是属于一个帮派,那么如果已知许多的关系,那么这里面有多少个帮派?
  • 问题表述很简单,那么我们应该怎么去解决这样一个问题呢?如果用平凡的解法,不借助其他数据结构,复杂度比较大,使用并查集可以大大减小时间复杂度,思路也更加清晰

结构

并查集的主要思想是分组,开始的时候所有人自己是一组,随着条件的加入,将不同的人分到一组去,那么如何去实现呢?

  • 首先使用一个数组,开始的时候set[i] = i,也就是自己和自己一组,如果得到一个信息,说1号和2号是一个组的,那么set[1]=set[2]也就是把2所在的组号赋值给1,或者反过来一样,这就是合并操作;至于查找操作,并查集数组是一种树状的逻辑结构,只需要递归查找x == set[x]的根节点就能够找到这一个数字是在哪一个集合里面,可以画图辅助理解
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x !=y) set[x] = set[y];
}

路径压缩

递归的好处是书写方便,但是带来比较大的时间消耗,我们可以在查找的过程中顺便修改父节点的set值,让每一个元素都直接跟祖先节点,这样可以大大减小时间

int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}

只需要在递归的每一层顺便修改set数组即可

按秩合并

树的层数影响搜索的效率,所以使用一个rank数组记录层数,按照两棵树的rank大小来判断将谁合并到谁那里去,在这里应该把矮的树合并到高树中,这样可以减少树的层数,假设相反,那么高树势必拉长矮树,这样层数增加,查找效率也就降低了

void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(rank[x] > rank[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(rank[x] == rank[y]) rank[y]++;
	}
}

例题

首先是模板题
洛谷1551
模板

#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(rank[x] > rank[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(rank[x] == rank[y]) rank[y]++;
	}
}
int main(){
    
    
	int n,m,p,x,y;
	cin>>n>>m>>p;
	for(int i=1;i<=n;i++) set[i] = i;
	for(int i=0;i<m;i++){
    
    
		cin>>x>>y;
		UNION(x,y);
	}for(int i=0;i<p;i++){
    
    
		cin>>x>>y;
		if(FIND_PATH(x) == FIND_PATH(y)) cout<<"Yes";
		else cout<<"No";
		cout<<endl;
	}
	return 0;
}

poj2236
题目大意:n台电脑,给定距离d,处于距离d以内的电脑之间能联系,开始的时候电脑都是坏的,每次指令O可以修复一台电脑,指令S询问两台电脑之间能否联系

  • 思路是并查集,每次O指令将距离之内的好的电脑和现在修好的放在一个集合,S指令直接查询即可
  • 合并操作有些费时,但是时间放得很宽
#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
    
    
	int x,y;
}node[MAXN];
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(rank[x] > rank[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(rank[x] == rank[y]) rank[y]++;
	}
}
int dis(NODE X,NODE Y){
    
    
	int dx = X.x - Y.x;
	int dy = X.y - Y.y;
	return (dx * dx + dy * dy);
}
int vis[MAXN];
int main(){
    
    
	char c;
	int n,d,m,p;
	cin>>n>>d;
	for(int i=1;i<=n;i++) set[i] = i;
	for(int i=1;i<=n;i++) cin>>node[i].x>>node[i].y;
	while(cin>>c){
    
    
		if(c == 'O'){
    
    
			cin>>m;
			for(int i=1;i<=n;i++){
    
    
				if(vis[i]&&i!=m&&dis(node[m],node[i])<=d*d){
    
    
					UNION(i,m);
				}
			} 
			vis[m] = 1;
		}else if(c == 'S'){
    
    
			cin>>m>>p;
			if(FIND_PATH(m) == FIND_PATH(p)) cout<<"SUCCESS"<<endl;
			else cout<<"FAIL"<<endl;
		}
	}
	return 0;
}

poj1611
模板题

#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
    
    
	int x,y;
}node[MAXN];
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(rank[x] > rank[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(rank[x] == rank[y]) rank[y]++;
	}
}
int main(){
    
    
	int n,m,p,x,y;
	while(cin>>n>>m){
    
    
		if(n == 0&&m == 0) break;
		for(int i=0;i<n;i++) set[i] = i;
		while(m--){
    
    
			cin>>p>>x;
			for(int i=1;i<p;i++){
    
    
				cin>>y;
				UNION(x,y);
			}
		}
		int ans = 0;
		for(int i=0;i<n;i++){
    
    
			if(FIND_PATH(i) == FIND_PATH(0)) ans++;
		}cout<<ans<<endl;
	}
	return 0;
}

poj1182

  • 注意一共只有A、B、C三种动物,且有关系A吃B,B吃C,C吃A
  • 那么这题可以考虑使用三个数组记录,但是还有另一种考虑方法就是可以将数组开大一些,用x,x+n,x+2*n这样三个区间去表示这三种动物的集合
  • 如果两种动物属于同类动物,那么对应的x,x+n,x+2n、y,y+n,y+2n应该分别处于相同集合(这里的意思是x和y可能处于不同的区间,比如x和y都是A类动物);谎话的判断是x和y不在同一个集合,也就是x和y+n或者x和y+2*n处于同一个集合
  • 如果两种动物存在x吃y的关系,那么可能是A吃B,也可能是B吃C,还可能是C吃A,所以要把x、y处于这样三个集合的部分都进行合并操作;谎话的判断是x和y处于同一个集合或者y吃x(注意这里)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
    
    
	int x,y;
}node[MAXN];
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(rank[x] > rank[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(rank[x] == rank[y]) rank[y]++;
	}
}
int main(){
    
    
	int n,k,op,x,y;
	cin>>n>>k;
	for(int i=1;i<=3*n;i++) set[i] = i;
	int ans = 0;
	while(k--){
    
    
		scanf("%d%d%d",&op,&x,&y);
		if(x<=0||y<=0||x>n||y>n){
    
    
			ans++;
			continue;
		}
		if(op == 2&&x == y){
    
    
			ans++;
			continue;
		}
		if(op == 1){
    
    
			if(FIND_PATH(x) == FIND_PATH(y+n)||FIND_PATH(x) == FIND_PATH(y+2*n)){
    
    
				ans++;
				continue;
			}
			UNION(x,y);
			UNION(x+n,y+n);
			UNION(x+2*n,y+n*2);
		}else if(op == 2){
    
    
			if(FIND_PATH(x) == FIND_PATH(y)||FIND_PATH(x) == FIND_PATH(y + 2*n)){
    
    
				ans++;
				continue;
			}
			UNION(x,y+n);
			UNION(x+n,y+2*n);
			UNION(x+2*n,y);
		}
	}
	cout<<ans;
	return 0;
}

hdu1272
这个题乍一看好像最小生成树,实际上有点类似Kruskal,按照题目要求,不能出现环,并且图应该是连通图,如果出现环,那么一定有一条边他的两个顶点处于同一集合,用并查集解决;如果图不是连通图,那么图应该可以分成两个集合,也就是说图有两个根节点,这可以通过遍历set数组得到

扫描二维码关注公众号,回复: 13132966 查看本文章
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int RANK[MAXN];
int vis[MAXN];
struct NODE{
    
    
	int x,y;
}node[MAXN];
int FIND_PATH(int x){
    
    
	if(x == set[x]) return x;
	return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
    
    
	x = FIND_PATH(x);
	y = FIND_PATH(y);
	if(x == y) return;
	if(RANK[x] > RANK[y]) set[y] = set[x];
	else{
    
    
		set[x] = set[y];
		if(RANK[x] == RANK[y]) RANK[y]++;
	}
}
int main(){
    
    
	int n,m;
	for(int i=1;i<=100000;i++) set[i] = i;
	bool flag = true;
	int num;
	int cnt = 0;
	while(scanf("%d%d",&n,&m)){
    
    
		if(n == -1&&m == -1) break;
		if(n == 0&&m == 0){
    
    
			for(int i=1;i<=100000;i++){
    
    
				if(set[i] == i&&vis[i]) cnt++;
			}if(cnt>1) flag = false;
			for(int i=1;i<=100000;i++) set[i] = i;
			for(int i=1;i<=100000;i++) vis[i] = 0;
			cnt = 0;
			if(flag) cout<<"Yes"<<endl;
			else cout<<"No"<<endl;
			flag = true;
		}else{
    
    
			if(FIND_PATH(n) == FIND_PATH(m)) flag=false;
			vis[n] = vis[m] = 1;
			UNION(n,m);	
		}
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/roadtohacker/article/details/105880553