并查集入门

并查集

并查集是一种树型的高级数据结构,主要用于处理不想交集合的合并及查询问题。它在计算机科学中有着广泛的应用。例如求解最小生成树(克鲁斯卡尔算法)、亲戚关系的判定、确定无向图的连通子图个数、最小公共祖先问题等,都要用到并查集。

集合

集合是数学中最基本的构造之一,将一组满足某种性质的对象放在一起就形成了集合。集合中包含的对象称为集合中的元素,集合中的元素是无序而且唯一的。常用大写英文字母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.

集合中元素的存储

  1. 数组存储数组一旦定义,其大小就固定不变。
  2. 链表链表可以很容易的动态生成和释放,所以增减节点是很方便的,完全不用考虑数组那样的长度问题。删除也很方便。但因为涉及到指针,实现起来很容易出错。
  3. 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个操作:

  1. 合并两个集合
  2. 查询一个元素在哪个集合
  3. 查询两个元素是否属于同一个集合

并查集操作示例

OperationDisjoint 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}
 123456
Merge(a,b)113456
Merge(b,e)113416
Merge(c,f)113413
Merge(b,f)111411

Query(a,e) :检查a,e的编号
算法复杂度
Query—O(1); Nerge—O(N)

用树结构表示集合

Init:
1

Merge(a,b):
2

Merge(b,e):
3

Merge(c,f):
4

Merge(b,f):
5

Mege(b,f):
将f所在树挂在b所在树的直接子树
开设父亲节点指示数组Par,Par[i]代表第i个元素的父亲。若元素i是树根,则Par[i] = i
6

Query(b,f)
简单比较b和f所在的根节点是否相同
7

缺点
树可能层次太深,以至于查树根太慢

Merge(d,c), Merge(c,b), Merge(b,a) …
8

解决方案一:根据树的层次进行合并
1、每个节点(元素)维护一个Rank表示子树最大可能高度
2、较小Rank的树连到较大Rank的树的根部

Link(x, y)
1
2
3
4
5
6
7
//new code
if (Rank[x] > Rank[y])
   Par[y] = x; else
   Par[x] = y;
   if (Rank[x] == Rank[y])
     Rank[y]++;
1
2
//old code Par[y] = x;
GET_PAR(a) 求a的根节点
1
2
3
4
if (Par[a] == a)
   return a; else    
   return GET_PAR(par[a]);
Query(a,b) //O(logN)
1
return GET_PAR(a) == GET_PAR(b);
Merge(a,b) //O(logN)
1
Link(GET_PAR(a), GET_PAR(b));

改进方法二:路径压缩
将GET_PAR中查找路径上的节点直接指向根
9

GET_PAR(a)
1
2
3
4
//new code
if (par[a] != a)
   Par[a]=GET_PAR(par[a]); return par[a];
1
2
3
4
5
//old code
if (par[a] == a)
   retrun a; else 
   return GET_PAR(par[a]);

在解决方案二存在的情况下,解决方案一失去了优化效果!

完整代码:

获取根节点
1
2
3
4
5
int get_par( int u) {
     if (par[a] != a)
         par[a] = get_par(par[a]);
     return par[a];
}
判断两个元素是否在一个集合中
1
2
3
int query( int a, int b) {
     Return get_par(a) == get_par(b);
}
合并两个集合
1
2
3
void merge( int a, int b) {
     par[get_par(a)] = get_par(b);
}

应用篇

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

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 带限制的作业排序问题(贪心+并查集)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值