并查集介绍和常用模板
前言: 并查集(Union-find set 也叫Disjoint Sets)是图论里面一种用来判断节点之间是否连通的数据结构,学会使用它可以处理一些跟节点连通性的问题。它有两个很重要的方法:
接下来我们先看一个例子,看看怎么判断节点之间是否相连,怎么把两个集合合并到一起:
先看第一个问题,怎么判断两个节点是否相连?
在上面这张图里面我们肉眼可以看到(0,1,2)是连在一起,(3,4)是连在一起,(5)是单独存在的,这也是前置客观存在的条件。
那么我们怎么判断0和3是不是连到一起了呢?思路是这样,我们把这三块看成是三个团队,(0,1,2)是一个团队,队长是0,(3,4)是一个团队,队长是3,(5)是一个团队,队长是5。判断逻辑是,两个元素的队长是相同的,就认为在一个团队里面。所以更加计算机化的语言描述是,三个集合,每个集合都有一个根节点,如果根节点是相同的,就是在一个集合里面。因此你要想到,我们需要存储每个节点对应的根节点,这样才能判断出两个节点是否在同一个集合内。
再看第二个问题,怎么将两个集合合并?
看过问题一后,你可能就已经有一些思路了,既然只要是根节点相同就是同一个集合,判断是否相连,那么我是不是合并两个集合的时候只要根节点改成同一个就行了?没错,就是这样。合并的时候就把某个集合的根节点改成另外一个集合的根节点就行了。这里引申出一个问题,应该把哪个集合的根节点修改掉?后面我们讲到按照秩来合并集合的时候会讲到,一般情况,随便用哪个集合的根节点都可以。
并查集常用模板: 1.QuickFind: 我们直接快进到代码部分,因为有了前面的例子的铺垫,再来讲代码应该就比较好理解了,说明我都放到代码的注释里面了。
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 public class UnionFind1 { // 首先我们创建了一个数组 用来保存每个元素的根节点 public int root[]; // 然后我们根据元素的数量 初始化数组,数组的下标就是节点,数组的值就是元素i的根节点,一开始每个元素的根节点都是自己 public UnionFind1(int size) { root = new int[size]; for (int i = 0; i < size; i++) { root[i] = i; } } // find找根节点,直接返回x对应的根节点root[x] public int find(int x) { return root[x]; } // union合并两个元素x,y public void union(int x, int y) { // 先找到各自的根节点 一开始都是元素自己 int rootX = find(x); int rootY = find(y); if (rootX != rootY) { // 因为两个根节点不一样 所以我们随便取一个元素的根节点作为新的根节点 // 这里取的rootX为新的根节点,然后把原来所有根节点是rootY的都修改为rootX for (int i = 0; i < root.length; i++) { if (root[i] == rootY) { root[i] = rootX; } } } } ; // 判断两个元素是否相连 其实就是判断根节点是否相同 是对find()的一种运用 public boolean connected(int x, int y) { return find(x) == find(y); } public static void main(String[] args) throws Exception { // 下面的例子就更好理解了,我们先初始化并查集一共6个元素,就是上面图中的元素 // 在图中的连接情况 把0,1,2连起来,3,4连起来,并用connected()测试下元素的连通性 // 最后我们把元素5加入到1的集合中,测试下2和5的连通性,发现确实2和5也相连了,程序测试完成。 UnionFind1 uf = new UnionFind1(6); // 0-1-2 3-4 5 uf.union(0, 1); uf.union(0, 2); uf.union(3, 4); // true System.out.println(uf.connected(1, 2)); // false System.out.println(uf.connected(1, 3)); // false System.out.println(uf.connected(4, 5)); // 0-1-2 3-4 5 uf.union(1, 5); // true System.out.println(uf.connected(2, 5)); } }
这个模板为什么要叫QuickFind呢?因为这个模板的find方法比较快,是O(1)的时间复杂度,但是union方法就比较麻烦得元素都遍历一遍,复杂度是O(N)
2.QuickUnion quickUnion主要是union的操作比较快,直接将x,y元素中任意一个元素变成另外一个元素的爸爸,操作是O(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 49 50 51 52 53 54 public class UnionFind2 { public int [] root; public UnionFind2 (int size) { root = new int [size]; for (int i = 0 ; i < size; i++) { root[i] = i; } } public int find (int x) { while (root[x] != x) { return root[x] = find(root[x]); } return x; } public void union (int x, int y) { int xRoot = find(x); int yRoot = find(y); if (xRoot != yRoot) { root[yRoot] = xRoot; } } public boolean connected (int x, int y) { return find(x) == find(y); } public static void main (String[] args) throws Exception { UnionFind2 uf = new UnionFind2 (6 ); uf.union(0 , 1 ); uf.union(0 , 2 ); uf.union(3 , 4 ); System.out.println(uf.connected(1 , 2 )); System.out.println(uf.connected(1 , 3 )); System.out.println(uf.connected(4 , 5 )); uf.union(1 , 5 ); System.out.println(uf.connected(2 , 5 )); } }
3.按秩合并的QuickUnion 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 public class UnionFind3 { // 在QuickUnion的基础上增加了rank数组来记录 int root[]; int rank[]; // 初始化的时候rank默认都是1 public UnionFind3(int size) { root = new int[size]; rank = new int[size]; for (int i = 0; i < size; i++) { root[i] = i; rank[i] = 1; } } public int find(int x) { while (x != root[x]) { x = root[x]; } return x; } // 如果两个元素的秩相同就随机取一个元素为秩更大的 否则就是谁的秩大 谁就是爸爸 public void union(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { if (rank[rootX] > rank[rootY]) { root[rootY] = rootX; } else if (rank[rootX] < rank[rootY]) { root[rootX] = rootY; } else { root[rootY] = rootX; rank[rootX] += 1; } } } public boolean connected(int x, int y) { return find(x) == find(y); } public static void main(String[] args) throws Exception { // 下面的例子就更好理解了,我们先初始化并查集一共6个元素,就是上面图中的元素 // 在图中的连接情况 把0,1,2连起来,3,4连起来,并用connected()测试下元素的连通性 // 最后我们把元素5加入到1的集合中,测试下2和5的连通性,发现确实2和5也相连了,程序测试完成。 UnionFind3 uf = new UnionFind3(6); // 0-1-2 3-4 5 uf.union(0, 1); uf.union(0, 2); uf.union(3, 4); // true System.out.println(uf.connected(1, 2)); // false System.out.println(uf.connected(1, 3)); // false System.out.println(uf.connected(4, 5)); // 0-1-2 3-4 5 uf.union(1, 5); // true System.out.println(uf.connected(2, 5)); } }
4.路径压缩的QuickUnion 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 public class UnionFind4 { int root[]; public UnionFind4(int size) { root = new int[size]; for (int i = 0; i < size; i++) { root[i] = i; } } // 优化点在这里 再查找父节点的同时会把根节点赋值给递归过程的其他元素 public int find(int x) { if (x == root[x]) { return x; } return root[x] = find(root[x]); } public void union(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { root[rootY] = rootX; } }; public boolean connected(int x, int y) { return find(x) == find(y); } public static void main(String[] args) throws Exception { // 下面的例子就更好理解了,我们先初始化并查集一共6个元素,就是上面图中的元素 // 在图中的连接情况 把0,1,2连起来,3,4连起来,并用connected()测试下元素的连通性 // 最后我们把元素5加入到1的集合中,测试下2和5的连通性,发现确实2和5也相连了,程序测试完成。 UnionFind4 uf = new UnionFind4(6); // 0-1-2 3-4 5 uf.union(0, 1); uf.union(0, 2); uf.union(3, 4); // true System.out.println(uf.connected(1, 2)); // false System.out.println(uf.connected(1, 3)); // false System.out.println(uf.connected(4, 5)); // 0-1-2 3-4 5 uf.union(1, 5); // true System.out.println(uf.connected(2, 5)); } }
5.按秩Union并且路径压缩: 这个没有太多要说的,就是3,4模板和合并,可以同时解决查询慢和防止并查集变成链表的情况。
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 public class UnionFind5 { private int[] root; private int[] rank; public UnionFind5(int size) { root = new int[size]; rank = new int[size]; for (int i = 0; i < size; i++) { root[i] = i; rank[i] = 1; } } public int find(int x) { if (x == root[x]) { return x; } return root[x] = find(root[x]); } public void union(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { if (rank[rootX] > rank[rootY]) { root[rootY] = rootX; } else if (rank[rootX] < rank[rootY]) { root[rootX] = rootY; } else { root[rootY] = rootX; rank[rootX] += 1; } } } public boolean connected(int x, int y) { return find(x) == find(y); } public static void main(String[] args) throws Exception { // 下面的例子就更好理解了,我们先初始化并查集一共6个元素,就是上面图中的元素 // 在图中的连接情况 把0,1,2连起来,3,4连起来,并用connected()测试下元素的连通性 // 最后我们把元素5加入到1的集合中,测试下2和5的连通性,发现确实2和5也相连了,程序测试完成。 UnionFind5 uf = new UnionFind5(6); // 0-1-2 3-4 5 uf.union(0, 1); uf.union(0, 2); uf.union(3, 4); // true System.out.println(uf.connected(1, 2)); // false System.out.println(uf.connected(1, 3)); // false System.out.println(uf.connected(4, 5)); // 0-1-2 3-4 5 uf.union(1, 5); // true System.out.println(uf.connected(2, 5)); } }
用并查集解决问题: 这里我们来看一道用并查集解决的算法题leetcode-200岛屿数量
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 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。 示例 1: 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1 示例 2: 输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3 提示: m == grid.length n == grid[i].length 1 <= m, n <= 300 grid[i][j] 的值为 '0' 或 '1'
这道题目可以用bfs/dfs解决,也可以用并查集,因为我们刚学习完并查集,所以就学以致用。
解题思路:
遍历整个图,将是邻近的岛屿元素全部用并查集连起来,也就是把1连起来,最后统计parent有多少个,就是有多少岛屿(不过这道题m*n比较大,遍历会超时)。当然我们也可以在union的时候判断出岛屿的数量,最后直接返回。
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 82 83 84 85 86 87 88 class UnionFind { public int root[]; // 用来统计最后有多少个岛屿 int num; public UnionFind(char[][] grid) { int row = grid.length; int col = grid[0].length; root = new int[row * col]; for (int r = 0; r < row; r++) { for (int c = 0; c < col; c++) { if (grid[r][c] == '1') { // 如果是岛屿的话 num就增加 num++; // 将二维坐标准换为一维的 root[r * col + c] = r * col + c; } } } } // 优化点在这里 再查找父节点的同时会把根节点赋值给递归过程的其他元素 public int find(int x) { if (x == root[x]) { return x; } return root[x] = find(root[x]); } public void union(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { root[rootY] = rootX; num--; } } } public int numIslands(char[][] grid) { int row = grid.length; int col = grid[0].length; UnionFind uf = new UnionFind(grid); int sea = 0; for (int r = 0; r < row; r++) { for (int c = 0; c < col; c++) { if (grid[r][c] == '1') { // 处理完之后 赋值为另外一个值 这样下次就不会重复遍历到这个值 grid[r][c] = '0'; if (r - 1 >= 0 && grid[r - 1][c] == '1') { uf.union(r * col + c, (r - 1) * col + c); } if (r + 1 < row && grid[r + 1][c] == '1') { uf.union(r * col + c, (r + 1) * col + c); } if (c - 1 >= 0 && grid[r][c - 1] == '1') { uf.union(r * col + c, r * col + c - 1); } if (c + 1 < col && grid[r][c + 1] == '1') { uf.union(r * col + c, r * col + c + 1); } } } } return uf.num; } @Test public void test1() { Assert.assertEquals(3, numIslands(new char[][]{ new char[]{'1', '1', '0', '0', '0'}, new char[]{'1', '1', '0', '0', '0'}, new char[]{'0', '0', '1', '0', '0'}, new char[]{'0', '0', '0', '1', '1'} })); Assert.assertEquals(1, numIslands(new char[][]{ new char[]{'1', '1', '1'}, new char[]{'0', '1', '0'}, new char[]{'1', '1', '1'}, })); }