并查集
并查集是一种树型的高级数据结构,主要用于处理不想交集合的合并及查询问题。它在计算机科学中有着广泛的应用。例如求解最小生成树(克鲁斯卡尔算法)、亲戚关系的判定、确定无向图的连通子图个数、最小公共祖先问题等,都要用到并查集。
集合
集合是数学中最基本的构造之一,将一组满足某种性质的对象放在一起就形成了集合。集合中包含的对象称为集合中的元素,集合中的元素是无序而且唯一的。常用大写英文字母A、B、C等来表示集合,并用x∈A来表示x是集合A中的元素。
集合的并、交、差:
由A、B集合的全体元素组成的集合为A与B的并集,记作A∪B;A与B的公共元素组成的集合称为A与B的交集,记作A∩B;属于集合A而不属于集合B的元素组成的集合称为A减B的差,记作A-B.
集合中元素的存储
- 数组存储数组一旦定义,其大小就固定不变。
- 链表链表可以很容易的动态生成和释放,所以增减节点是很方便的,完全不用考虑数组那样的长度问题。删除也很方便。但因为涉及到指针,实现起来很容易出错。
- vector它有数组的优点,而且又不必考虑数组那样可能越界的情况。
并查集的概念
在某些应用中,我们要检查两个元素是否属于同一个集合,或者将两个不同的集合合并为一个集合。这是不相交集合经常处理的两种操作:查找和合并,我们成为并查集。
查找find:查找一个指定元素属于哪个集合。对于判断两个元素是否属于同一个集合是非常有用的。
合并union:将两个集合合并为1个集合。
如何标示一个集合
选择集合中某个固定的元素作为集合的代表,让它唯一的标识整个集合。一般来说,选取的代表是任意的。也就是说,到底选择集合中的哪个元素作为它的代表是无关紧要的。
树的思想
在并查集中,我们对于集合的表示利用树的思想,一个集合可以看做一棵树,树根即代表该集合的标识。如果两个集合在同一个树中,则它们是同一个集合;合并两个集合,即是对两棵树进行合并。
Find(x)
返回元素x所属集合的代表.
Query(x, y)
询问元素x和元素y是否在一个集合中。只需判断find(x)和find(y)是否相等即可。如果相等,说明他们属于同一个集合,否则它们不属于同一个集合。
Union(x, y)
将包含元素x的集合(假设为Sx)和包含元素y的集合(假设为Sy)合并为一个新的集合(即这两个集合的并集),所得到的并集可以用它的任何一个元素来做代表,但在实践中,一般都是选择Sx或者Sy的代表作为并集的代表。
N个不同的元素分布在若干个互不相交集合中,需要进行一下3个操作:
- 合并两个集合
- 查询一个元素在哪个集合
- 查询两个元素是否属于同一个集合
并查集操作示例
Operation | Disjoint sets | |||||
初始状态 | {a} | {b} | {c} | {d} | {e} | {f} |
Merge(a,b) | {a,b} | {c} | {d} | {e} | {f} | |
Query(a,c) | False | |||||
Query(a,b) | True | |||||
Merge(b,e) | {a,b,e} | {c} | {d} | {f} | ||
Merge(c,f) | {a,b,e} | {c,f} | {d} | |||
Query(a,e) | True | |||||
Query(c,b) | False | |||||
Merge(b,f) | {a,b,c,e,f} | {d} | ||||
Query(a,e) | True | |||||
Query(d,e) | False |
土算法
给集合编号
| {a} | {b} | {c} | {d} | {e} | {f} |
1 | 2 | 3 | 4 | 5 | 6 | |
Merge(a,b) | 1 | 1 | 3 | 4 | 5 | 6 |
Merge(b,e) | 1 | 1 | 3 | 4 | 1 | 6 |
Merge(c,f) | 1 | 1 | 3 | 4 | 1 | 3 |
Merge(b,f) | 1 | 1 | 1 | 4 | 1 | 1 |
Query(a,e) :检查a,e的编号
算法复杂度
Query—O(1); Nerge—O(N)
用树结构表示集合
Mege(b,f):
将f所在树挂在b所在树的直接子树
开设父亲节点指示数组Par,Par[i]代表第i个元素的父亲。若元素i是树根,则Par[i] = i
缺点
树可能层次太深,以至于查树根太慢
Merge(d,c), Merge(c,b), Merge(b,a) …
解决方案一:根据树的层次进行合并
1、每个节点(元素)维护一个Rank表示子树最大可能高度
2、较小Rank的树连到较大Rank的树的根部
|
|
|
|
|
改进方法二:路径压缩
将GET_PAR中查找路径上的节点直接指向根
|
|
在解决方案二存在的情况下,解决方案一失去了优化效果!
完整代码:
|
|
|
应用篇
POJ1611 The Suspects
n个学生分属m个团体,(0 < n <= 30000 ,0 <= m <= 500) 一个学生可以属于多个团体。一个学生疑似患病,则它所属的整个团体都疑似患病。已知0号学生疑似患病,以及每个团体都由哪些学生构成,求一共多少个学生疑似患病。
解法:最基础的并查集,把所有可疑的都并一块。
Sample Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output
n4
n1
n1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#include <iostream>
#include <cstdio>
using
namespace
std;
const
int
MAX = 30000;
int
n,m,k;
int
parent[MAX+10];
int
total[MAX+10];
//total[GetParent(a)]是a所在的group的人数
int
GetParent(
int
a) {
//获取a的根,并把a的父节点改为根
if
( parent[a]!= a)
parent[a] =
GetParent(parent[a]);
return
parent[a];
}
void
Merge(
int
a,
int
b) {
int
p1 = GetParent(a);
int
p2 = GetParent(b);
if
( p1 == p2 )
return
;
total[p1] += total[p2];
parent[p2] = p1;
}
int
main() {
while
(
true
) {
scanf
(
"%d%d"
,&n,&m);
if
( n == 0 && m == 0)
break
;
for
(
int
i= 0; i < n; ++i) {
parent[i] = i;
total[i] = 1;
}
for
(
int
i= 0; i < m; ++i) {
int
h,s;
scanf
(
"%d"
,&k);
scanf
(
"%d"
,&h);
for
(
int
j = 1; j < k; ++j) {
scanf
(
"%d"
,&s);
Merge(h,s);
}
}
printf
(
"%d\n"
,total[GetParent(0)]);
}
return
0;
}
|
POJ 1988 Cube stacking
有N(N<=30,000)堆方块,开始每堆都是一个方块。方块编号1 –N. 有两种操作:
M x y :表示把方块x所在的堆,拿起来叠放到y所在的堆上。
C x : 问方块x下面有多少个方块。
操作最多有P (P<=100,000)次。对每次C操作,输出结果。
解法:
除了parent数组,还要开设
sum数组:记录每堆一共有多少方块。
若parent[a] = a, 则sum[a]表示a所在的堆的方块数目。
under数组,under[i]表示第i个方块下面有多少个方块。
under数组在堆合并和路径压缩的时候都要更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#include <iostream>
#include <cstdio>
using
namespace
std;
const
int
MAX = 31000;
int
parent[MAX];
int
sum[MAX];
// 若parent[i]=i,sum[i]表示砖块i所在堆的砖块数目
int
under[MAX];
// under[i]表示砖块i下面有多少砖块
int
GetParent(
int
a) {
//获取a的根,并把a的父节点改为根
if
( parent[a] == a)
return
a;
int
t = GetParent(parent[a]);
under[a] += under[parent[a]];
parent[a] = t;
return
parent[a];
}
void
Merge(
int
a,
int
b) {
//把b所在的堆,叠放到a所在的堆。
int
n;
int
pa = GetParent(a);
int
pb= GetParent(b);
if
( pa == pb)
return
;
parent[pb] = pa;
under[pb] = sum[pa];
//under[pb] 赋值前一定是0,因为parent[pb] = pb,pb一定是原b所在堆最底下的
sum[pa] += sum[pb];
}
int
main() {
int
p;
for
(
int
i= 0; i< MAX; ++ i) {
sum[i] = 1;
under[i] = 0;
parent[i] = i;
}
scanf
(
"%d"
,&p);
for
(
int
i= 0; i < p; ++ i) {
char
s[20];
int
a,b;
scanf
(
"%s"
,s);
if
( s[0] ==
'M'
) {
scanf
(
"%d%d"
,&a,&b);
Merge(b,a);
}
else
{
scanf
(
"%d"
,&a);
GetParent(a);
printf
(
"%d\n"
,under[a]);
}
}
return
0;
}
|
POJ 1182 食物链
三类动物A、B、C,A吃B,B吃C,C吃A。
给出K句话来描述N个动物(各属于A、B、C三类之一)之间的关系,格式及意义如下:
1 X Y:表示X与Y是同类;
2 X Y:表示X吃Y。
K句话中有真话有假话,当一句话满足下列三条之一时,这句话就是假话,否则就是真话。1)当前的话与前面的某些真的话冲突,就是假话;2)当前的话中X或Y比N大,就是假话;3)当前的话表示X吃X,就是假话。
求假话的总数。
输入:
第一行是两个整数N和K,以一个空格分隔。以下K行每行是三个正整数D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。若D=1,则表示X和Y是同类。若D=2,则表示X吃Y。
输出:
只有一个整数,表示假话的数目。
约束条件:
1 <= N <= 50000,0 <= K <= 100000。
一个容易想到的思路:
用二维数组s存放已知关系:
S[X][Y] = -1:表示X与Y关系未知;
S[X][Y] = 0:表示X与Y是同类;
S[X][Y] = 1:表示X吃Y;
S[X][Y] = 2:表示Y吃X。
对每个读入的关系s(x,y),检查S[x][y]:
若S[x][y]=s,则继续处理下一条;
若S[x][y] = -1,则令S[x][y]=s,并更新S[x][i]、S[i][x]、S[y][i]和S[i][y] (0<i<=n)。
若S[x][y] != s且S[x][y] != -1,计数器加1。
复杂度:
以上算法需要存储一个N×N的数组,空间复杂度为O(N2)。
对每一条语句
进行关系判定时间为O(1)
加入关系时间为O(N)
总的时间复杂度为O(N*K)
0<=N<=50000,0<=K<=100000,复杂度太高。
进一步分析
对于任意a≠b,a、b属于题中N个动物的集合S,当且仅当S中存在一个有限序列(P1, P2, …, Pm)(m≥0)使得aP1、P1P2、…、Pm-1Pm、Pmb(或m=0时的ab)之间的相对关系均已确定时,b对a的相对关系才可以确定。
由上面可知,我们不需要保留每对个体之间的关系,只需要为每对已知关系的个体保留一条路径aP1P2…Pmb(m≥0)其中aP1、P1P2、…、Pm-1Pm、Pmb之间的关系均为已知。两两关系已知的动物们,构成一个group
解决方案
使用并查集
用结点表示每个动物,边表示动物之间的关系。采用父结点表示法,在每个结点中存储该结点与父结点之间的关系。
parent数组:parent[i]表示i的父节点
relation数组:relation[i]表示i和父节点的关系
初始状态下,每个结点单独构成一棵树。
读入a,b关系描述时的逻辑判断:
分别找到两个结点a、b所在树的根结点ra、rb,并在此过程中计算a与ra、b与rb之间的相对关系。
若ra!=rb,此句为真话,将a、b之间的关系加入;
若ra=rb,则可计算出r(a,b)=f( r(a,ra) , r(b,rb) )
若读入的关系与r(a,b)矛盾,则此句为假话,计数器加1;
若读入的关系与r(a,b)一致,则此句为真话。
一些练习
POJ 2492 A Bug„s Life
法一:深度优先遍历
每次遍历记录下该点是男还是女,
只有: 男->女,女->男满足,
否则,找到同性恋二分图匹配,结束程序
法二:并查集
POJ 2524 最基础的并查集
POJ 1182 并查集的拓展有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A。也就是说:只有三个group
POJ 1861并查集+自定义排序+贪心求“最小生成树”
POJ 1703并查集的拓展
POJ 2236 并查集的应用需要注意的地方:1、并查集;2、N的范围,可以等于1001;3、从N+1行开始,第一个输入的可以是字符串。
POJ 2560最小生成树
法一:Prim算法;法二:并查集实现Kruskar算法求最小生成树
POJ 1456 带限制的作业排序问题(贪心+并查集)