并查集
一、原理、结构
并查集是一种可以用来管理元素分组的数据结构,可以高效的进行合并和查询。实际就是,有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 < = 5000 , m < = 5000 , p < = 5000 ) (n<=5000,m<=5000,p<=5000) (n<=5000,m<=5000,p<=5000),分别表示有 n n n个人, m m m个亲戚关系,询问 p p p对亲戚关系。
以下 m m m行:每行两个数 M i , M j , 1 < = M i , M j < = N , M_i,M_j,1<=M_i,M_j<=N, Mi,Mj,1<=Mi,Mj<=N,表示 M i M_i Mi和 M j M_j Mj具有亲戚关系。
接下来 p p p行:每行两个数 P i , P j , P_i,P_j, Pi,Pj,询问 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
1-N编号。每个动物都是
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 < = N < = 50 , 000 1 <= N <= 50,000 1<=N<=50,000)和 K K K句话( 0 < = K < = 100 , 000 0 <= K <= 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 A、B、C中的哪一类, 3 3 3种都有可能,因次 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,假设现在是 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;
}