并查集就是一种一边查找一边并集的数据结构,简单的并查集经常应用于朋友圈等题目,即:x和y是朋友,y和z是朋友,则x和z是朋友,下面给出一组数据表示xx和yy是朋友,最后问一共有多少个朋友圈。这类问题一般用并查集解决比较快。
下面明确并查集做的事情:
1.查集,即查找某个元素是否包含在一个集合里面
2.并集,即将两个集合合并到一块
知道并查集要做的事后,实现它就比较简单了,数据结构书上教我们的是用树的方法来实现并查集,即用一个数组来模拟树,然后利用树的特性来实现,但是那个一下子会有点难以理解,所以我们一开始可以用java自带的集合类来模拟实现,帮助理解。
下面是用java集合来实现并查集的代码:
1 import java.io.*; 2 import java.util.*; 3 4 public class javaSet_union_find_set { 5 static int n; 6 static List<Set<Integer>>list = new ArrayList<Set<Integer>>(); 7 public static void main(String[] args){ 8 Scanner in = new Scanner(new BufferedInputStream(System.in)); 9 n = in.nextInt(); 10 //初始化,为每个人单独建立一个集合 11 for(int i=1;i<=n;i++){ 12 Set<Integer>set = new HashSet<Integer>(); 13 set.add(i); 14 list.add(set); 15 } 16 int m = in.nextInt(); 17 for(int i=0;i<m;i++){ 18 int x = in.nextInt(); 19 int y = in.nextInt(); 20 union(x,y); 21 } 22 for(int i=0;i<list.size();i++){ 23 System.out.println("集合"+i+":"); 24 System.out.print("( "); 25 for(int number:list.get(i)){ 26 System.out.print(number+" "); 27 } 28 System.out.print(") "); 29 } 30 System.out.println(list.size()); 31 } 32 //查集操作,返回一个集合,即元素所在的集合 33 public static Set<Integer> find(int x){ 34 for(Set set:list){ 35 if(set.contains(x)){ 36 return set; 37 } 38 } 39 return null; 40 } 41 //并集操作,将两个元素所在的集合合并 42 public static void union(int a,int b){ 43 Set<Integer>set1 = find(a); 44 Set<Integer>set2 = find(b); 45 //当两个集合不相等的时候,合并他们 46 if(set1!=set2){ 47 list.remove(set1); 48 list.remove(set2); 49 set1.addAll(set2); 50 list.add(set1); 51 } 52 } 53 }
所用到的思想就是简单并查集的思想,下面我们假设一组数据来具体说明,
测试数据:
6 4
1 2
1 3
1 4
5 6
第一行第一个数为n,即一共有多少个人,第二个数为m,即以下给出m对关系,接着m行,每行两个数,表示第x个人和第y个人是朋友关系
答案:
2
最后应该是,1,2,3,4是一组朋友关系,5,6是一组朋友关系,故一共有两个朋友圈
用并查集来解决这个问题,就是要运用集合的特性,即两个集合有并集操作。
故,我们一开始给每一个人创建一个集合,即每个人都是单独的一个集合,下面给出初始关系,用()表示一个集合。
初始关系为:(1),(2),(3),(4),(5),(6)
对m行数据进行处理:
1和2是朋友,那么包含1的集合和包含2的集合合并,则现在关系为:
(1,2),(3),(4),(5),(6)
1和3是朋友,并集后:
(1,2,3),(4),(5),(6)
1和4是朋友,并集后:
(1,2,3,4),(5),(6)
5和6是朋友,并集后:
(1,2,3,4),(5,6)
这就是完整的一次集合操作,最后数组list的长度即为朋友圈的个数
上面用java的集合类Set模拟了一下并查集的具体操作,每次查集的时间复杂度为数组的长度即O(N),每次并集的复杂度为原本java集合类并集的复杂度,有m次查询,粗略计算时间复杂度为O(mn),即O(N),也算是线性复杂度吧。
接下来是传统做法,用数组模拟树来实现,用数组模拟树实现并查集如下所示:
一开始每个人都是独立的集合 (1),(2),(3),(4),(5),(6) 1和2是朋友,并集后: (1),(3),(4),(5),(6) | (2) 1和3是朋友,并集后: (1) ,(4),(5),(6) | (2) (3) 1和4是朋友,并集后: (1) ,(5),(6) / | (4)(2)(3) 5和6是朋友,并集后: (1) , (5) / | | (4)(2)(3) (6)
上面只是模拟了并查集操作,具体的指向为具体的程序的启发函数决定。
使用数组模拟树的并查集有以下几点需要注意:
1.路径压缩操作,在并集操作中很可能出现下面这个情况:
(1) (5) | | (2) (6) | (3) | (4)
当出现这种树的时候,进行查集操作的时候会额外消耗更多的查询时间。
所以这时要运用路径压缩的方法,即每次进行查询操作的时候都进行路径压缩,即查询子节点的时候,都将子节点指向根节点
2.并集操作中怎么合并两个集合,应该根据什么来进行合并
这里有两个启发函数可以选择:
1.根据节点数量进行合并,即将节点少的树结合到节点数量多的树中
2.根据树的高度进行合并,即将高度小的树合并到高度高的树
下面两个启发函数分别举例,先是使用启发函数1+路径压缩的做法,代码如下:
1 import java.io.*; 2 import java.util.*; 3 4 public class union_find_set { 5 //父节点数组,father[x]=y表示x的父节点为y 6 //如果father[x]小于0,则说明x是一个根节点,此时father[x]的绝对值是 7 //这颗树的节点数量 8 static int[] father; 9 static int n; 10 public static void main(String[] args){ 11 Scanner in = new Scanner(new BufferedInputStream(System.in)); 12 n = in.nextInt(); 13 int m = in.nextInt(); 14 father = new int[n+1]; 15 //初始化父节点数组 16 for(int i=0;i<=n;i++){ 17 father[i] = -1; 18 } 19 for(int i=0;i<m;i++){ 20 int x = in.nextInt(); 21 int y = in.nextInt(); 22 union(x,y); 23 } 24 for(int i=1;i<=n;i++){ 25 System.out.println("find("+i+"):"+find(i)); 26 } 27 } 28 //查集操作,查找x元素所属的集合的根节点,同时进行路径压缩操作 29 public static int find(int x){ 30 int root = x; 31 while(root>=0){ 32 root = father[root]; 33 } 34 //此时已找到根节点为root 35 36 //路径压缩 37 while(x!=root){ 38 int temp = x; 39 x = father[x]; //x继续往上一个父节点追溯,直到根节点 40 father[temp] = root; //路径压缩,把所有子节点都直接指向根节点 41 } 42 43 return root; 44 } 45 //并集操作,将两个集合合并,将x元素所属的集合和y元素所属的集合合并 46 public static void union(int x,int y){ 47 int fx = find(x); 48 int fy = find(y); 49 //temp的绝对值为两棵树的节点数量总和 50 int temp = fx + fy; 51 //说明x元素所属的集合节点比y元素所属的集合少 52 //那么将节点少的集合合并到节点多的集合上 53 if(fx>fy){ 54 father[x] = y; 55 father[y] = temp; 56 }else{ 57 father[x] = temp; 58 father[y] = x; 59 } 60 } 61 }
使用启发函数1的做法,那么模拟树的数组有一点要注意的就是,为负数的时候表示这是一个根节点,并且此负数的相反数为该棵树的节点总数
一下是使用启发函数2的做法,代码如下:
1 import java.io.*; 2 import java.util.*; 3 4 //使用启发函数2的做法 5 public class union_find_set_another { 6 static int[] father; //记录每个节点的父节点,father[x]=y表示为x元素的父节点为y元素 7 static int[] rank; //记录每棵树的高度,rank[x]=h表示为x元素所含集合的深度为h 8 static int n; 9 public static void main(String[] args){ 10 Scanner in = new Scanner(new BufferedInputStream(System.in)); 11 n = in.nextInt(); 12 father = new int[n+1]; 13 rank = new int[n+1]; 14 int m = in.nextInt(); 15 //初始化father数组 16 //令每个节点一开始都指向自己 17 for(int i=0;i<=n;i++){ 18 father[i] = i; 19 } 20 //初始化rank数组 21 for(int i=0;i<=n;i++){ 22 rank[i] = 1; 23 } 24 for(int i=0;i<m;i++){ 25 int x = in.nextInt(); 26 int y = in.nextInt(); 27 union(x,y); 28 } 29 for(int i=1;i<=n;i++){ 30 System.out.println("find("+i+"):"+find(i)); 31 } 32 } 33 public static int find(int x){ 34 if(x!=father[x]){ 35 return find(father[x]); 36 } 37 return x; 38 } 39 public static void union(int x,int y){ 40 int fx = find(x); 41 int fy = find(y); 42 if(fx==fy){ 43 //说明两个元素同属一个集合,这种情况直接返回 44 return; 45 } 46 //此处应用启发函数2,根据树的高度来进行合并 47 //这里我们将高度小的合并到高度大的树上 48 if(rank[fx]>rank[fy]){ 49 father[fy] = fx; 50 }else{ 51 if(rank[fx]==rank[fy]){ 52 //当两棵树一样高的时候,则增加其中一棵的高度 53 rank[fy]++; 54 } 55 father[fx] = fy; 56 } 57 } 58 }
启发函数2中额外使用了一个rank数组来记录树的高度,并让father数组严格遵守father[x]=y表示x的父节点为y这一个规定,使用find函数查找根节点会返回根节点的值,而不是节点数量,这一点比启发函数1来的好用,再加上代码短,思路清晰这一点,我是建议都用启发函数2来实现的>.<
简单并查集的归纳就到这里了,如果想验证代码的正确性的话,下面有几个题目可以选择:
https://leetcode.com/problems/friend-circles/description/ leetcode的朋友圈问题,经典的并查集应用
http://codevs.cn/problem/2597/ 团伙问题,敌人的敌人是朋友是需要注意的一点
最后说几句,这篇文章是我从《算法竞赛宝典》数据结构一章归纳总结而来,如果有什么问题或纰漏,请您指出,本人感激不尽~~~