并查集

本文最后更新于:2 个月前

并查集

并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的 合并查询 问题。 它支持两种操作:

适用并查集的情况

已知一堆叶子,只知道每个叶子的父节点。如何查找每个叶子的祖先节点(判断两个点是否属于同一集合);如何将所有叶子合并进一棵树。
这种问题就适合使用并查集进行叶子的整理,查找每个叶子究竟属于哪一棵树。

方便起见使用一个一维数组来储存每个叶子节点极其父节点。

1
int[] parent;  parent[a] = b; //代表a的父亲节点是b,并规定祖先的父亲节点是自己。 

再贴一个来自OI-wiki的接地气解释

通俗地讲一个故事:几个家族进行宴会,但是家族普遍长寿,所以人数众多。由于长时间的分离以及年龄的增长,这些人逐渐忘掉了自己的亲人,只记得自己的爸爸是谁了,而最长者(称为「祖先」)的父亲已经去世,他只知道自己是祖先。

宴会上,一个家族的祖先突然对另一个家族说:我们两个家族交情这么好,不如合成一家好了。另一个家族也欣然接受了。

并查集的“查”

解决方法并没有那么难想,只要一个一个叶子往前推,直到找到祖先叶子为止。所以可以使用递归思想。

1
2
3
4
5
6
7
8
int[] parent;
int find(int x) {
// 寻找x的祖先
if (parent[x] == x) // 如果x是祖先则返回
return x;
else
return find(parent[x]); // 如果不是则x的爸爸问x的爷爷
}

路径压缩

上面的查确实可以查找到祖先,但是要经过所有的叶子节点,效率明显不高。所以如果只想找到每个叶子的祖先,只需要每个叶子都成为祖先的直接子节点,即parent[a]直接储存a的祖先,这样就可以达到路径压缩减小时间复杂度的目的。

1
2
3
4
5
6
7
8
9
10
11
int[] parent;
int find(int x) {
// 寻找x的祖先
if (parent[x] == x){ // 如果x是祖先则返回
return x;
}
else{
parent[x] = find(parent[x]); // 查找x的祖先直到找到代表,顺手路径压缩
return find(parent[x]); // 如果不是则x的爸爸问x的爷爷
}
}

并查集的“并”

将已知的几棵树合在一起成为一棵树,最简单粗暴的方法就是使一棵树的祖先节点成为另一棵树祖先节点的子节点。

1
2
3
4
5
6
void unionSet(int x, int y) {
// x 与 y 所在树合并
x = find(x);
y = find(y);
parent[x] = y; // 把 x 的祖先变成 y 的祖先的儿子
}

启发式合并(按秩合并)

因为并查集这个数据结构只支持集合的合并与查询操作,所以合并时选择将点数更少深度更小的树移动为其他树的字数可以使得查找时的最坏时间复杂度更低。

当然并不是每一次都能找到点数既是最小深度又是最小的树,只能使用一个特征作为选取移动树的参考。

。。。。。后续待补充

java 中的并查集

Java嘛,面向对象语言,这种数据结构必然得给他封装成一个类日后用起来才方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UnionFind {
int[] parent;

public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}

public void union(int index1, int index2) {
parent[find(index2)] = find(index1);
}

public int find(int index) {
if (parent[index] != index) {
parent[index] = find(parent[index]);
}
return parent[index];
}
}

经典例题

冗余连接

在本问题中, 树指的是一个连通且无环的无向图。

输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/redundant-connection
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

示例 1:

1
2
3
4
5
6
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
1
/ \
2 - 3

示例 2:

1
2
3
4
5
6
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
| |
4 - 3

注意:

输入的二维数组大小在 3 到 1000。
二维数组中的整数在1到N之间,其中N是输入数组的大小。

解答

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
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int[] parent = new int[edges.length+1];
for(int i = 1;i<edges.length+1;i++) parent[i] = i;

for(int i = 0;i<edges.length;i++)
{
if(find(parent, edges[i][0])!= find(parent, edges[i][1]))
//在冗余的边出现之前,两个点已经出现在同一集合了
{
union(parent, edges[i][0], edges[i][1]);
}
//如果一个边的两个端点已经是同一集合的了,这条边就是多余的,返回
else return edges[i];
}
return null;
}
//**
这个方法就是并查集当中的“查”,查找祖先并返回;
其中else中的语句还顺便进行了路径缩短,使得所有同一家族的元素指向的均为最高级的祖先
*/
public static int find(int[] parent, int index)
{
if(parent[index]==index) return index;
else return parent[index]=find(parent, parent[index]);
}
//**
这个方法就是并查集当中的“并”, 可以将一条新的树枝加入原有的图中
*/
public static int[] union(int[] parent, int index1, int index2)
{
parent[find(parent, parent[index2])] = find(parent, index1);
return parent;
}

public static void main(String[] args)
{
int[][] test = {{1.2},{1,3},{2,3}};
System.out.println(findRedundantConnection(test).toString());
}
}

碎碎念

小弟当初初入力扣就遇到并查集月,每天的每日一题都是并查集……
跟着做了几道本来以为自己可能永远也忘不了这个知识点了,
结果几个月之后看见群里有大佬讨论这东西发现自己忘光光了……
遂作此篇🤐


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!