并查集

并查集

一、原理、结构

并查集是一种可以用来管理元素分组的数据结构,可以高效的进行合并和查询。实际就是,有N个元素的集合,一开始每一个元素本身就是一个集合,然后随着将不同的集合合并,然后就出现了联合很多集合的大的集合,这时候可以查询某两个元素是否为一个集合。

通俗的解释:摘自:原博客

话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的帮派,通过两两之间的朋友关系串联起来。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?

我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物。这样,每个圈子就可以这样命名“中国同胞队”美国同胞队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。

但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。

二、操作、实现

并查集通过一个一维数组来实现,本质上是维护一个森林。刚开始的时候,森林里的每一个结点都是一个集合,之后根据题意,逐渐将一个个集合合并。之后寻找时不断查找父节点,当查找到父结点为本身的结点时,这个结点就是祖宗结点。合并则是寻找这两个结点的祖宗结点,如果这两个结点不相同,则将其中右边的集合作为左边集合的子集(即靠左,靠右也是同一原理)。两种操作如下

  • 合并
  • 查询

准备工作

int f[n];
for(int i = 1; i <= n; i++) //初始化并查集,使得每一个元素都是一个集合
    f[i] = i;

查找

//递归实现
int find(int x) {
	if(x == f[x]) return x; //出现祖宗节点
	else return f[x] = find(f[x]); //不断递归
}
//非递归
inline int find(int x) {
	int r, i;
	r = x, i = x;
	while(f[r] != r) r = f[r]; //找到祖宗节点
	while(f[i] != r) i = f[i], f[i] = r; //路径压缩,将每一个节点的前一个都直接置为祖宗节点,方便快速查询
	return r;
}

合并

void unite(int x, int y) {
	int u, v;
	u = find(x); //找到x的祖宗节点
	v = find(y); //找到y的祖宗节点
	if(u != v) f[u] = v; //将x的祖宗变为y,完成合并
}

三、例题

 1、亲戚:
        若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

​ 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

输入

第一行:三个整数 n , m , p n,m,p n,m,p ( n &lt; = 5000 , m &lt; = 5000 , p &lt; = 5000 ) (n&lt;=5000,m&lt;=5000,p&lt;=5000) n<=5000,m<=5000,p<=5000,分别表示有 n n n个人, m m m个亲戚关系,询问 p p p对亲戚关系。

以下 m m m行:每行两个数 M i , M j , 1 &lt; = M i , M j &lt; = N , M_i,M_j,1&lt;=M_i,M_j&lt;=N, MiMj1<=MiMj<=N表示 M i M_i Mi M j M_j Mj具有亲戚关系。

接下来 p p p行:每行两个数 P i , P j , P_i,P_j, PiPj询问 P i P_i Pi P j P_j Pj是否具有亲戚关系。

输出
P P P行,每行一个’Yes’或’No’。表示第 i i i个询问的答案为“具有”或“不具有”亲戚关系。

样例输入

6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6

样例输出

Yes
Yes
No

代码

#include <iostream>
#define N 6000
using namespace std;

int f[N], n, m, p;
void init() {
	for(int i = 1; i <= n; i++)
		f[i] = i;
}
int find(int x) { //查找x的祖宗节点
	if(x == f[x]) return x;
	else return f[x] = find(f[x]);
}
void unite(int x, int y) { //合并x,y
	int u, v;
	u = find(x);
	v = find(y);
	if(u != v) f[u] = v;
}
bool same(int x, int y) { //判断x,y是否在同一集合
	int u = find(x);
	int v = find(y);
	if(u == v) return 1;
	else return 0;
}
int main() {
	int x, y;
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m >> p;
	init();
	for(int i = 1; i <= m; i++) {
		cin >> x >> y;
		unite(x, y);
	}
	for(int i = 1; i <= p; i++) {
		cin >> x >> y;
		if(same(x, y)) cout << "Yes\n";
		else cout << "No\n";
	}
	return 0;	
}

 2、食物链(POJ 1182):
动物王国中有三类动物 A , B , C A,B,C A,B,C,这三类动物的食物链构成了有趣的环形。 A A A B B B B B B C C C C C C A A A
现有 N N N个动物,以 1 - N 1-N 1N编号。每个动物都是 A A A, B B B, C C C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N N N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示 X X X Y Y Y是同类。
第二种说法是"2 X Y",表示 X X X Y Y Y
此人对 N N N个动物,用上述两种说法,一句接一句地说出 K K K句话,这 K K K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;

  • 当前的话中 X X X Y Y Y N N N大,就是假话;

  • 当前的话表示 X X X X X X,就是假话。

    你的任务是根据给定的 N N N 1 &lt; = N &lt; = 50 , 000 1 &lt;= N &lt;= 50,000 1<=N<=50,000)和 K K K句话( 0 &lt; = K &lt; = 100 , 000 0 &lt;= K &lt;= 100,000 0<=K<=100,000),输出假话的总数。

输入

第一行是两个整数 N N N K K K,以一个空格分隔。
以下 K K K行每行是三个正整数 D D D X X X Y Y Y,两数之间用一个空格隔开,其中 D D D表示说法的种类。
D = 1 D=1 D=1,则表示 X X X Y Y Y是同类。
D = 2 D=2 D=2,则表示 X X X Y Y Y

输出
只有一个整数,表示假话的数目。

样例输入

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

样例输出

3

由于 3 3 3类动物形成了食物链,分别为 A A A B B B C C C类,可以由前 n n n个表示 A A A类,中间 n n n个表示 B B B类,后面 n n n个表示 C C C类,总共 3 N 3N 3N个,其中,当我们需要合并两个相同种类的动物时,由于不知道具体是 A 、 B 、 C A、B、C ABC中的哪一类, 3 3 3种都有可能,因次 A 、 B 、 C A、B、C ABC中全都要合并相应的种类,这样并不会影响结果(合并前需要先做判断),由于是 A A A B B B B B B C C C C C C A A A,假设现在是 x x x y y y,为表示这样的关系,可以巧妙地通过合并 A A A类中的 x x x B B B类中的 y y y B B B中的 x x x C C C中的 y y y C C C中的 x x x A A A中的 y y y(因为同样不知道 x x x y y y的种类,因此 3 3 3种可能都要维护,所有合并 3 3 3次),来表示这种吃的关系,这样可以很方便清楚的维护关系

代码

#include <iostream>
#define N 50000
using namespace std;

int f[N * 3 + 10];
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' or c > '9') {if(c == '-') f = -1; c = getchar();}
	while(c >= '0' and c <= '9') {x = x * 10 + c - 48; c = getchar();} 
	return f * x;
} 

void init(int x) { 
	for(int i = 1; i <= x; i++)
		f[i] = i;
}

inline int find(int x) { 
	int r, i;
	r = x, i = x;
	while(f[r] != r) r = f[r];
	while(f[i] != r) i = f[i], f[i] = r; 
	return r;
}
inline void unite(int x, int y) {
	int u = find(x);
	int v = find(y);
	f[u] = v;
}

int main() {
	int n, k, d, x, y, ans = 0;
	n = read(); k = read();
	init(n * 3);
	for(int i = 1; i <= k; i++) {
		d = read(); x = read(); y = read();
		if(x > n or y > n or (d == 2 and x == y)) {
			ans++;
			continue;
		}
		if(d == 1) {
			if(find(x) == find(y + n) or find(x) == find(y + 2 * n)) { //如果x吃y或者被y吃,x和y肯定不是同类
				ans ++;
				continue;
			}
			unite(x, y);
			unite(x + n, y + n);
			unite(x + 2 * n, y + 2 * n);
		}else {
			if(find(x) == find(y) or find(x) == find(y + 2 * n)) { //x和y是同类,或者x被y吃,则x肯定不能吃y
				ans ++;
				continue;
			}
			unite(x, y + n);
			unite(x + n, y + 2 * n);
			unite(x + 2 * n, y);
		}

	}
	cout << ans;
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值