数据结构-Disjoint Sets 并查集

image.png

背景

在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往空间上过大,计算机无法承受;即使在空间上勉强通过,时间复杂度也极高。

简介

并查集(Disjoint Sets )是一种属性结构(通过数组存储),主要用于处理多个集合求交集问题。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。主要是通过集合数组+元素数组来进行存储。

支持的两种操作

  • 查找(Find):确定某个元素处于哪个子集;
  • 合并(Union):将两个子集合并成一个集合。

用编号最小的元素标记所在集合;定义一个数组 set[1..n] ,其中set[i] 表示元素i 所在的集合

rank 1 2 3 4 5 6 7 8 9 10
parent 1 2 1 4 1 6 6 6 4 4

实现

数组:一个整数数组,称为parent[]。如果我们处理n个项目,数组的第i个元素代表第i个项目。更准确地说,数组的第i个元素是第i个项的父元素。这些关系创建一个或多个虚拟树。

:这是一个不相交的集合。如果两个元素位于同一棵树中,则它们位于同一集合中。每个树的根节点(或最上面的节点)称为集合的代表。每一组都有一个独特的代表。识别代表的一个简单规则是,如果我是一个集合的代表,那么parent[i]=i。如果我不是他的集合的代表,那么可以沿着树向上移动,直到找到代表。

初始化

建立两个数组分别为rank,parent初始化其长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DisjointUnionSets {

int[] rank;
int[] parent;
int n;

// 构建
public DisjointUnionSets(int n) {
rank = new int[n];
parent = new int[n];
this.n = n;
makeSet();
}

//创建n个集合,每个集合中有一个项目
void makeSet() {
for (int i = 0; i < n; i++) {
//初始化,所有元素都在自己的集合中。
parent[i] = i;
}
}
}

查找

可以通过递归地遍历父数组来实现,直到我们到达一个节点,该节点是其自身的父节点。

1
2
3
4
5
6
7
8
9
public int find(int i) {
// 如果 i 是自己的父节点(代表)
if (parent[i] == i) {
return i;
} else {
//如果当前parent不是代表继续沿着树向上移动,直到找到代表
return find(parent[i]);
}
}

合并

需要两个元素作为输入。并使用find操作查找其集合的代表,最后将其中一棵树(代表集合)放在另一棵树的根节点下,从而有效地合并树和集合。

1
2
3
4
5
6
7
8
9
//将包含i的集合统一起来,还有包括j
void union(int i, int j) {
//查找包含i的集合的代表(或根节点)
int irep = this.Find(i);
//对包含j的集合也要这样做
int jrep = this.Find(j);
//使i的代表的父节点成为j的代表(有效地将所有i的集合移动到j的集合中)
this.Parent[irep] = jrep;
}

完整实现

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.example.algorithm.disjointset;

public class DisjointUnionSets {

int[] rank;
int[] parent;
int n;

// 构建
public DisjointUnionSets(int n) {
rank = new int[n];
parent = new int[n];
this.n = n;
makeSet();
}

//创建n个集合,每个集合中有一个项目
void makeSet() {
for (int i = 0; i < n; i++) {
//初始化,所有元素都在自己的集合中。
parent[i] = i;
}
}

// 返回x集合的代表
int find(int x) {
// 查找x是其元素的集合的代表
if (parent[x] != x) {
// 如果x不是自己的父,那么x就不是他的集合的代表
parent[x] = find(parent[x]);
//所以我们递归地调用其父节点上的Find,并将i的节点直接移动到这个集合的代表下
}
return parent[x];
}

// 合并包含x的集合和包含x的集合
void union(int x, int y) {
// 找到两组的代表集合
int xRoot = find(x), yRoot = find(y);
// 元素在同一个集合中,不需要将任何东西联合起来。
if (xRoot == yRoot) return;
// 如果x的排序小于y的排序
if (rank[xRoot] < rank[yRoot])
// Then move x under y so that depth of tree remains less
//然后将x移动到y下方,使树的深度保持较小
parent[xRoot] = yRoot;
// 否则如果y的排序小于x的排序
else if (rank[yRoot] < rank[xRoot])
// 然后将y移到x下,使树的深度保持较小
parent[yRoot] = xRoot;
else // 如果x和y的排序大小一样
{
// 然后将y移到x下(无论哪个方向)
parent[yRoot] = xRoot;
// 并将结果树的秩增加1
rank[xRoot] = rank[xRoot] + 1;
}
}

public static void main(String[] args) {
// 假设有5个人的ID为0、1、2、3和4
int n = 5;
DisjointUnionSets dus = new DisjointUnionSets(n);
// 0是2的朋友
dus.union(0, 2);
// 4是2的朋友
dus.union(4, 2);
// 3是1的朋友
dus.union(3, 1);
// 检查4是否是0的朋友
if (dus.find(4) == dus.find(0)) System.out.println("Yes");
else System.out.println("No");
// 检查1是否是0的朋友
if (dus.find(1) == dus.find(0)) System.out.println("Yes");
else System.out.println("No");
}
}

案例

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Solution {
class UnionFind {
int count;
int[] parent;
int[] rank;

public UnionFind(char[][] grid) {
count = 0;
int m = grid.length;
int n = grid[0].length;
parent = new int[m * n];
rank = new int[m * n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
parent[i * n + j] = i * n + j;
++count;
}
rank[i * n + j] = 0;
}
}
}

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

public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx] += 1;
}
--count;
}
}

public int getCount() {
return count;
}
}

public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}

int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
UnionFind uf = new UnionFind(grid);
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
grid[r][c] = '0';
if (r - 1 >= 0 && grid[r-1][c] == '1') {
uf.union(r * nc + c, (r-1) * nc + c);
}
if (r + 1 < nr && grid[r+1][c] == '1') {
uf.union(r * nc + c, (r+1) * nc + c);
}
if (c - 1 >= 0 && grid[r][c-1] == '1') {
uf.union(r * nc + c, r * nc + c - 1);
}
if (c + 1 < nc && grid[r][c+1] == '1') {
uf.union(r * nc + c, r * nc + c + 1);
}
}
}
}

return uf.getCount();
}
}

6106. 统计无向图中无法互相到达点对数(mid)

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
class Solution {
int[] parent;
public long countPairs(int n, int[][] edges) {
parent = new int[n];
//初始化,所有元素都在自己的集合中。
for (int i = 0; i < n; i++) {
parent[i] = i;
}
//根据给定的边构建集合
for(int[] edge:edges){
union(edge[0],edge[1]);
}
//确定每个集合拥有的元素个数
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < n; i++) {
int root = find(i);
map.put(root,map.getOrDefault(root,0)+1);
}
//计算结果
long ret = 0;
for(int key:map.keySet()){
int cnt = map.get(key);
ret +=(long)cnt * (n-cnt);
}
return ret >> 1;
}
public int find(int i) {
// 如果 i 是自己的父节点(代表)
if (parent[i] == i) {
return i;
} else {
//如果当前parent不是代表继续沿着树向上移动,直到找到代表
return find(parent[i]);
}
}
//将包含i的集合统一起来,还有包括j
public void union(int i, int j) {
//查找包含i的集合的代表(或根节点)
int irep = this.find(i);
//对包含j的集合也要这样做
int jrep = this.find(j);
//使i的代表的父节点成为j的代表(有效地将所有i的集合移动到j的集合中)
this.parent[jrep] = irep;
}
}

资料

https://www.geeksforgeeks.org/disjoint-set-data-structures/?ref=gcse


数据结构-Disjoint Sets 并查集
https://mikeygithub.github.io/2021/01/08/yuque/数据结构-Disjoint Sets 并查集/
作者
Mikey
发布于
2021年1月8日
许可协议