并查集的优化及应用
Posted hyserendipity
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并查集的优化及应用相关的知识,希望对你有一定的参考价值。
2018-05-01 15:13:08
并查集是一个时空复杂度非常优越的数据结构,并且通过优化后其复杂度为<O(1),O(n)>。
并查集的优化主要有两个方面:
- 路径压缩
- 按rank来合并
路径压缩:
按rank合并:
public class UnionFindSet { private int[] parent; private int[] rank; public UnionFindSet(int n) { parent = new int[n + 1]; rank = new int[n + 1]; for (int i = 0; i < n + 1; i++) { parent[i] = i; rank[i] = 1; } } public int find(int i) { if (parent[i] != i) parent[i] = find(parent[i]); return parent[i]; } public boolean union(int i, int j) { int pi = find(i); int pj = find(j); if (pi == pj) return false; if (rank[pi] > rank[pj]) parent[pj] = pi; else if (rank[pi] < rank[pj]) parent[pi] = pj; else { parent[pj] = pi; rank[pi]++; } return true; } }
-
684. Redundant Connection
问题描述:
问题求解:
树形下的无向图判断环路问题,图的描述方式是采用边集。
并查集本身就是树形结构,而树是一个无向图,具体来说,树是一个无环的连通图,所以本题可以直接使用并查集来进行求解。
public int[] findRedundantConnection(int[][] edges) { UnionFindSet ufs = new UnionFindSet(edges.length + 1); int[] res = new int[2]; for (int[] pair : edges) { if (!ufs.union(pair[0], pair[1])) { res[0] = Math.min(pair[0], pair[1]); res[1] = Math.max(pair[0], pair[1]); break; } } return res; }
2019.04.21
public int[] findRedundantConnection(int[][] edges) { int n = edges.length; int[] parent = new int[n + 1]; for (int i = 1; i <= n; i++) parent[i] = i; for (int[] edge : edges) { if (!union(parent, edge[0], edge[1])) { Arrays.sort(edge); return edge; } } return null; } private int find(int[] parent, int i) { if (parent[i] != i) { parent[i] = find(parent, parent[i]); } return parent[i]; } private boolean union(int[] parent, int i, int j) { int pi = find(parent, i); int pj = find(parent, j); if (pi == pj) return false; parent[pj] = pi; return true; }
-
547. Friend Circles
问题描述:
问题求解:
class Solution { public int findCircleNum(int[][] M) { UnionFindSet ufs = new UnionFindSet(M.length); int res = 0; for (int i = 0; i < M.length; i++) { for (int j = 0; j < i; j++) { if (M[i][j] == 1) ufs.union(i, j); } } for (int i = 0; i < ufs.parent.length; i++) if (ufs.parent[i] == i) res++; return res; } } class UnionFindSet { public int[] parent; private int[] rank; public UnionFindSet(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; rank[i] = 1; } } public int find(int i) { if (parent[i] != i) parent[i] = find(parent[i]); return parent[i]; } public boolean union(int i, int j) { int pi = find(i); int pj = find(j); if (pi == pj) return false; if (rank[pi] > rank[pj]) parent[pj] = pi; else if (rank[pi] < rank[pj]) parent[pi] = pj; else { parent[pj] = pi; rank[pi]++; } return true; } }
-
765. Couples Holding Hands
问题描述:
问题求解:
这里有一个O(n)的做法, 一次考虑两个凳子,假设他们不为夫妇,为了让这两个位置坐的恰好是一对夫妇,那么我们就需要调整其中一个人的位置,如此调整直到所有的夫妇相邻,交换的次数就是答案。下面给出证明。
将给定的row抽象成一个n个顶点的无向图(可能包含重边),例如:
(_ _) (_ _) ... (_ _)
(v1 ) (v2 ) ... (vn )
vi和vj之间存在边当且仅当vi和vj中存在一对夫妇,例如vi = (0,2),vj = (1, 4)存在一对夫妇(0, 1),而vi = (0, 2),vj = (1, 3)之间则存在两对夫妇(0, 1)(2, 3),此vi和vj存在重边。
考虑row数组形成的无向图,可以肯定要么是孤立的单个节点,要么是多个孤立的圈,例如row = [0, 1]是一个孤立的点、row = [0, 2, 1, 3]则包含一个圈v1, v2、row = [0, 3, 4, 1, 2, 5, 6, 8, 7, 9],包含两个孤立的圈v1, v2, v3和v4, v5。
对于一个圈来说,假设他有n个节点,那么至少需要n-1次交换即可让每个夫妇相邻。有了这个结论,假定row数组有n个点,m个孤立的圈,那么至少需要n-m次交换即可。
public int minSwapsCouples(int[] row) { int n = row.length / 2; int[] parent = new int[n]; int cnt = n; for (int i = 0; i < n; i++) parent[i] = i; for (int i = 0; i < n * 2; i += 2) { if (union(parent, row[i] / 2, row[i + 1] / 2)) cnt--; } return n - cnt; } private int find(int[] parent, int i) { if (parent[i] != i) { parent[i] = find(parent, parent[i]); } return parent[i]; } private boolean union(int[] parent, int i, int j) { int pi = find(parent, i); int pj = find(parent, j); if (pi == pj) return false; parent[pj] = pi; return true; }
- 947. Most Stones Removed with Same Row or Column
问题描述:
问题求解:
这个问题可以转化为求图中连通数的问题,也就是经典的陆地数量问题。对于x | y相等的两个石头我们需要建立他们之间的联系。
经典的连通子树问题可以使用dfs进行求解,从dfs的算法过程我们可以看到其实是一棵以起始点为root的树,因此,在这次dfs中我们总可以从叶子节点开始选取,知道最后只剩下root节点。
最终的答案就是所有的stones的数目 - 连通块的数目。
这里并不打算使用dfs来进行求解,将使用ufs来进行求解。
使用并查集并不需要那么形式化的专门使用一个类来进行表征,这就是一个简单的数据结构,只需要在使用前进行定义就好了,另外,由于parent的数目范围不确定,所以在很多时候使用数组并不是一个合适的选择,使用hash表更能方便我们解决问题,本题就是使用hash表来进行的并查集的实现。
public int removeStones(int[][] stones) { Map<Integer, Integer> parent = new HashMap<>(); int n = stones.length; for (int[] stone : stones) { union(parent, stone[0], stone[1] + 10000); } int cnt = 0; for (int key : parent.keySet()) { if (parent.get(key) == key) cnt++; } return n - cnt; } private int find(Map<Integer, Integer> parent, int i) { if (!parent.containsKey(i)) parent.put(i, i); if (parent.get(i) != i) { parent.put(i, find(parent, parent.get(i))); } return parent.get(i); } private boolean union(Map<Integer, Integer> parent, int i, int j) { int pi = find(parent, i); int pj = find(parent, j); if (pi == pj) return false; parent.put(pj, pi); return true; }
- 959. Regions Cut By Slashes
问题描述:
问题求解:
主要问题就是怎么将字符串的输入进行转化,这里采用的转化方式是将每个cell看成4个区域,0-3。根据不同的情况可以将各个区域进行合并,这样本题就又变成了并查集问题。
int n; public int regionsBySlashes(String[] grid) { n = grid.length; int[] parent = new int[n * n * 4]; for (int i = 0; i < parent.length; i++) parent[i] = i; int size = n * n * 4; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i > 0 && union(parent, getIdx(i - 1, j, 2), getIdx(i, j, 0))) size--; if (j > 0 && union(parent, getIdx(i, j - 1, 1), getIdx(i, j, 3))) size--; if (grid[i].charAt(j) != \'/\') { if (union(parent, getIdx(i, j, 0), getIdx(i, j, 1))) size--; if (union(parent, getIdx(i, j, 2), getIdx(i, j, 3))) size--; } if (grid[i].charAt(j) != \'\\\\\') { if (union(parent, getIdx(i, j, 0), getIdx(i, j, 3))) size--; if (union(parent, getIdx(i, j, 1), getIdx(i, j, 2))) size--; } } } return size; } private int find(int[] parent, int i) { if (parent[i] != i) { parent[i] = find(parent, parent[i]); } return parent[i]; } private boolean union(int[] parent, int i, int j) { int pi = find(parent, i); int pj = find(parent, j); if (pi == pj) return false; parent[pj] = pi; return true; } private int getIdx(int i, int j, int num) { return i * n * 4 + j * 4 + num; }
以上是关于并查集的优化及应用的主要内容,如果未能解决你的问题,请参考以下文章