ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

并查集

2022-08-10 13:54:30  阅读:219  来源: 互联网

标签:parent int 查集 find height ufs 节点


之前在介绍“图的一些基本概念”中提到了最小生成树,其中一种算法是克鲁斯卡尔(Kruskal's algorithm)算法,里面涉及了对环的判断。我们再回顾下算法的主要流程:

从最小的一个边开始连接,然后再连接第二小的边,且保证新加入的边不能和已经连接的顶点形成环。这样一直重复,最终连接起所有的顶点。

算法中提到的“形成环”是指新加入的边导致图形成了一个回路。类似图示这种结构,新加入的b-c边导致a b c顶点边构成了一个回路

                  图-1

如果从树的角度来看,即b和c的拥有同一个根节点a。如果层级多一些,类似如下

                            图-2

c e也拥有同一个根节点a。因此我们可以通过判断两个节点是否具有相同的根节点,来判断是否属于同一棵树,我们把这种操作叫做查询(find)。

由于算法是从最小边开始逐渐连接各个边的,不一定能保证新加入的边和之前边是相连的。如图这种情况,a-b和c-e这两个边在某个中间状态可能是分开的。

                              图-3

但最终的最小生成树必然要连接起各个顶点,也就必然要连接相应的边,我们把这种连接操作称为联合(union)。

有了查询和联合的这两个定义,我们再看并查集就很容易明白了。

所谓并查集(union-find set或者disjoint set)是用来查找元素和合并集合的一种数据类型。是对关系的处理,用来合并集合,确定元素是否属于某个集合。

示例中树结构可以理解成集合的一种具体表现形式。在图-2中,a-c c-e我们能推导出a-e也是连接的,只不过中间经过了c。连接也只是一种统称,具体形式可以是亲戚朋友关系、道路或电路连通等,都是对关系的一种表述。

 

接下来我们看下具体的数据结构和实现方法。先实现基础的查询和联合,再一步步完善。为了方便演示,我们假定有0-9共计10个节点,分别对应数组的下标0-9。每个数组值默认也是0-9,表示根节点是自身。

                                              图-4

 1 public class UnionFindSetV1 {
 2     private final int[] parent;
 3 
 4     public static void main(String[] args) {
 5         UnionFindSetV1 ufs = new UnionFindSetV1(10);
 6         System.out.println(Arrays.toString(ufs.parent));
 7         ufs.union(0, 1);
 8         ufs.union(1, 2);
 9         ufs.union(2, 3);
10         ufs.union(8, 9);
11         System.out.println(Arrays.toString(ufs.parent));
12         System.out.println("1 root: " + ufs.find(1));
13         System.out.println("2 root: " + ufs.find(2));
14     }
15 
16     public UnionFindSetV1(int num) {
17         parent = new int[num];
18         for (int i = 0; i < num; i++) {
19             //根节点初始化为自身
20             parent[i] = i;
21         }
22     }
23 
24     public int find(int i) {
25         //值等于自身说明是根节点
26         if (parent[i] == i) {
27             return i;
28         }
29         //递归查询所属的根节点
30         return find(parent[i]);
31     }
32 
33     public void union(int i, int j) {
34         int a = find(i);
35         int b = find(j);
36         //属于同一个根节点无需操作
37         if (a == b) {
38             return;
39         }
40         //后一个元素的根节点设置为第一个元素
41         parent[j] = i;
42     }
43 }

 输出

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 0, 1, 2, 4, 5, 6, 7, 8, 8]
1 root: 0
2 root: 0

从代码中可以看到,我们对节点(0, 1) (1, 2) (2, 3) (8, 9)分别执行了联合操作,这里规定了第一个元素当做根节点。在进行联合操作的时候,先查找两个元素所属的根节点,如果属于同一个根节点说明属于同一个集合,无需操作。否则直接把第二个元素的根节点更新为第一个元素。执行完毕,对应的数组和树的结构如下

                                                          图-5

 

                                    图-6 

执行查找操作时,先找到该元素的父节点,再查找父节点的父节点,直到发现父节点是自身值时。节点1和2的查找过程示意图如下,可以看到1只查找了一次,2则先找到1,再从1找到了0。

                  图-7

从查找根节点的操作过程中,我们不难发现随着节点不断连接,会导致树的高度不断增长,这样后续节点查找所属根节点时,导致查找步骤越来越多。例如连接(2, 3)后,当查找3的根节点时,步骤必然会加一。

                图-8

因此我们考虑如何减少树的高度。一种理想的情况是每个节点都直接存储它的根节点,也就是如下格式。

                     

                      图-9                                                                                                  图-10

如何让节点存储它的最终根节点呢?我们观察下find方法,其实执行此方法时,由于是递归调用是知道最终的根节点的,这里就可以修改为最终的根节点,这样后续再次查找时就可以直接取到最终的根节点了。把find方法调整为如下:

1 public int find(int i) {
2     //值等于自身说明是根节点
3     if (parent[i] == i) {
4         return i;
5     }
6     //递归查询所属的根节点,并把当前节点的根节点设置为查询到的最终根节点
7     parent[i] = find(parent[i]);
8     return parent[i];
9 }

同时调整下main方法的测试代码,增加

1 System.out.println("3 root: " + ufs.find(3));
2 System.out.println(Arrays.toString(ufs.parent));

最终输出

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 0, 0, 2, 4, 5, 6, 7, 8, 8]
1 root: 0
2 root: 0
3 root: 0
[0, 0, 0, 0, 4, 5, 6, 7, 8, 8]  #这里3的根节点已经被修改为0

我们把这种减少查找根节点步骤的操作称为路径压缩(path compression)

有了路径压缩,我们再看下合并操作是否有优化空间。 看下图-11这种情况,其实是有两种合并方法的,一种是8做根节点,另一种是7做根节点。虽然两种方法都实现了集合的合并,但方法1的高度是小于方法2的。而高度的增加会导致查询次数变多。

 

                                                       图-11

因此我们可以考虑记录每棵子树的高度,优先用高度较大的树当根节点,从而保证合并后的树高度维持不变。我们把这种方法称为按高度合并(union-by-height),或者按秩(rank)合并。对应代码中,需要引入一个记录高度信息的height数组,每个元素对应的初始高度为1。具体代码如下:

 1 public class UnionFindSetV3 {
 2     private final int[] parent;
 3     private final int[] height;
 4 
 5     public static void main(String[] args) {
 6         UnionFindSetV3 ufs = new UnionFindSetV3(10);
 7         System.out.println(Arrays.toString(ufs.parent) + " height " + Arrays.toString(ufs.height));
 8         ufs.union(0, 1);
 9         System.out.println("0-1 " + Arrays.toString(ufs.parent) + " height " + Arrays.toString(ufs.height));
10         ufs.union(2, 0);
11         System.out.println("2-0 " + Arrays.toString(ufs.parent) + " height " + Arrays.toString(ufs.height));
12     }
13 
14     public UnionFindSetV3(int num) {
15         parent = new int[num];
16         height = new int[num];
17         for (int i = 0; i < num; i++) {
18             //根节点初始化为自身
19             parent[i] = i;
20             //高度默认为1
21             height[i] = 1;
22         }
23     }
24 
25     public int find(int i) {
26         //值等于自身说明是根节点
27         if (parent[i] == i) {
28             return i;
29         }
30         //递归查询所属的根节点,并把当前节点的根节点设置为查询到的最终根节点
31         parent[i] = find(parent[i]);
32         return parent[i];
33     }
34 
35     public void union(int i, int j) {
36         int a = find(i);
37         int b = find(j);
38         //属于同一个根节点无需操作
39         if (a == b) {
40             return;
41         }
42         if (height[j] > height[i]) {
43             //高度大的做为根节点,且合并后高度不变
44             parent[i] = j;
45         } else {
46             parent[j] = i;
47             if (height[i] == height[j]) {
48                 //高度相等时的合并会导致高度加1
49                 height[i]++;
50             }
51         }
52     }
53 }

 输出

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] height [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
0-1 [0, 0, 2, 3, 4, 5, 6, 7, 8, 9] height [2, 1, 1, 1, 1, 1, 1, 1, 1, 1] #元素1对应的根节点是0,由于合并前0、1高度都是1,导致合并后0的高度变为2
2-0 [0, 0, 0, 3, 4, 5, 6, 7, 8, 9] height [2, 1, 1, 1, 1, 1, 1, 1, 1, 1] #当无高度判断时,仅以出现顺序为依据,默认2为根节点。当增加对高度判断后,元素0对应的高度2,大于元素2的高度0,因此以高度较大的0为根节点,合并后0的高度仍然是2。

 

总结

我们从克鲁斯卡尔算法引出了“并查集”的概念,并且基于数组结构实现了快速判断集合中两个元素间的关系(是否属于同一个根节点),同时也支持动态合并集合。另外为提升查询效率引入“路径压缩”的方法,减少了查询根节点的次数。在集合合并时又引入“按高度合并”的操作,降低了合并后树的高度。通过这些介绍,相信大家(我)对并查集会有更深地理解。

 

参考资料

Disjoint Set Union (Union Find) 

并查集入门-视频

数据结构与算法分析:Java语言描述(原书第3版)第八章-不相交集类

 

标签:parent,int,查集,find,height,ufs,节点
来源: https://www.cnblogs.com/binary220615/p/16571189.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有