作者: Grey
原文地址:使用並查集解決的相關問題
關於並查集的說明,見如下部落格:
相關題目
LeetCode 200. 島嶼數量
本題的解題思路參考部落格
LeetCode 547. 省份數量
主要思路
橫縱座標表示的是城市,因為城市是一樣的,所以只需要遍歷對角線上半區或者下半區即可,如果某個(i,j)
位置是1
,可以說明如下兩個情況
第一,i
這座城市和j
這座城市可以做union
操作。
第二,(j,i)
位置一定也是1。
遍歷完畢後,返回整個並查集中的集合數量即可。
完整程式碼
public static int findCircleNum(int[][] m) {
int n = m.length;
UF uf = new UF(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (m[i][j] == 1) {
uf.union(i, j);
}
}
}
return uf.setSize();
}
public static class UF {
int[] parent;
int[] help;
int[] size;
int sets;
public UF(int n) {
size = new int[n];
parent = new int[n];
help = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
sets = n;
}
public void union(int i, int j) {
if (i == j) {
return;
}
int p1 = find(i);
int p2 = find(j);
if (p2 != p1) {
int size1 = size[p1];
int size2 = size[p2];
if (size1 > size2) {
parent[p2] = p1;
size[p1] += size2;
} else {
parent[p1] = p2;
size[p2] += size1;
}
sets--;
}
}
public int find(int i) {
int hi = 0;
while (i != parent[i]) {
help[hi++] = i;
i = parent[i];
}
for (int index = 0; index < hi; index++) {
parent[help[index]] = i;
}
return i;
}
public int setSize() {
return sets;
}
}
LeetCode 305. 島嶼數量II
本題和LeetCode 200. 島嶼數量最大的區別就是,本題是依次給具體的島嶼,並查集要實時union
合適的兩個島嶼,思路也一樣,初始化整個地圖上都是水單元格,且整個地圖的島嶼數量初始為0,如果某個位置來一個島嶼,先將此位置的代表節點設定為本身,將此位置的集合大小設定為1,然後和其上下左右方向有島嶼的點union
一下,並將集合數量減一,返回集合數量即可,完整程式碼見:
public static List<Integer> numIslands2(int m, int n, int[][] positions) {
UF uf = new UF(m, n);
List<Integer> ans = new ArrayList<>();
for (int[] position : positions) {
ans.add(uf.connect(position[0], position[1]));
}
return ans;
}
public static class UF {
int[] help;
int[] parent;
int[] size;
int sets;
int row;
int col;
public UF(int m, int n) {
row = m;
col = n;
int len = m * n;
help = new int[len];
size = new int[len];
parent = new int[len];
}
private int index(int i, int j) {
return i * col + j;
}
private void union(int i1, int j1, int i2, int j2) {
if (i1 < 0 || i2 < 0 || i1 >= row || i2 >= row || j1 < 0 || j2 < 0 || j1 >= col || j2 >= col) {
return;
}
int f1 = index(i1, j1);
int f2 = index(i2, j2);
if (size[f1] == 0 || size[f2] == 0) {
// 重要:如果兩個都不是島嶼,則不用合併
return;
}
f1 = find(f1);
f2 = find(f2);
if (f1 != f2) {
int s1 = size[f1];
int s2 = size[f2];
if (s1 >= s2) {
size[f1] += s2;
parent[f2] = f1;
} else {
size[f2] += s1;
parent[f1] = f2;
}
sets--;
}
}
public int find(int i) {
int hi = 0;
while (i != parent[i]) {
help[hi++] = i;
i = parent[i];
}
for (int index = 0; index < hi; index++) {
parent[help[index]] = i;
}
return i;
}
public int connect(int i, int j) {
int index = index(i, j);
if (size[index] == 0) {
sets++;
size[index] = 1;
parent[index] = index;
// 去四個方向union
union(i - 1, j, i, j);
union(i, j - 1, i, j);
union(i + 1, j, i, j);
union(i, j + 1, i, j);
}
// index上本來就有島嶼,所以不需要處理
return sets;
}
}
LeetCode 130. 被圍繞的區域
和LeetCode 200. 島嶼數量問題一樣,這個題目也有DFS和並查集兩種解決方法。DFS方法的思路是,先遍歷最外圈(即:第一行,第一列,最後一行,最後一列),最外圈中的元素如果為O
,則做如下事情:
將這個元素和其相連的元素都設定為#
號(或者其他的只要不是原矩陣有的字元),我們可以簡單理解為,拿最外圈的O
元素去"解救"和其相連的所有元素,並打上一個標記。
然後再次遍歷整個矩陣,只要沒打上標記的(理解為沒被解救的),都設定為X
,其餘的都是O
。
DFS解法的完整程式碼見:
public static void solve(char[][] board) {
if (board == null || board.length == 0 || board[0] == null || board[0].length == 0) {
return;
}
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
free(i, 0, board);
}
if (board[i][n - 1] == 'O') {
free(i, n - 1, board);
}
}
for (int i = 0; i < n; i++) {
if (board[0][i] == 'O') {
free(0, i, board);
}
if (board[m - 1][i] == 'O') {
free(m - 1, i, board);
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = (board[i][j] != '#' ? 'X' : 'O');
}
}
}
public static void free(int i, int j, char[][] board) {
int m = board.length;
int n = board[0].length;
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != 'O') {
return;
}
board[i][j] = '#';
free(i + 1, j, board);
free(i - 1, j, board);
free(i, j + 1, board);
free(i, j - 1, board);
}
本題還有並查集的做法,即,將矩陣第一列,最後一列,第一行,最後一行中的所有O
節點的代表節點都設定為dump
節點,然後遍歷矩陣的其他位置,只要是O
字元的,就和其四個方向的節點進行union
操作,最後,再次遍歷整個矩陣,如果是O
且和dump
不是同一集合的,就算是沒有被解救的點,否則,都設定為X
。
完整程式碼如下:
public static void solve(char[][] board) {
if (board == null || board.length <= 2 || board[0].length <= 2) {
return;
}
int m = board.length;
int n = board[0].length;
// 所有周邊為O的點的代表節點都設定為dump
int dump = 0;
UF uf = new UF(m, n);
// 第一列和最後一列O字元的元素和dump點union一下
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
uf.union(dump, i, 0);
}
if (board[i][n - 1] == 'O') {
uf.union(dump, i, n - 1);
}
}
// 第一行和最後一行O字元的元素和dump點union一下
for (int i = 0; i < n; i++) {
if (board[0][i] == 'O') {
uf.union(dump, 0, i);
}
if (board[m - 1][i] == 'O') {
uf.union(dump, m - 1, i);
}
}
// 其他位置,只要是O字元,就和上下左右的O字元union一下
for (int i = 1; i < m - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (board[i][j] == 'O') {
int index = uf.index(i, j);
if (board[i - 1][j] == 'O') {
uf.union(index, i - 1, j);
}
if (board[i][j - 1] == 'O') {
uf.union(index, i, j - 1);
}
if (board[i + 1][j] == 'O') {
uf.union(index, i + 1, j);
}
if (board[i][j + 1] == 'O') {
uf.union(index, i, j + 1);
}
}
}
}
// 最後判斷哪些不是和dump點在同一集合的O點,這些都會被X吞沒
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O' && !uf.isSameSet(dump, i, j)) {
board[i][j] = 'X';
}
}
}
}
public static class UF {
int[] help;
int[] size;
int[] parent;
int row;
int col;
public UF(int m, int n) {
row = m;
col = n;
// 多一個位置,用於存dump
int len = m * n + 1;
help = new int[len];
size = new int[len];
parent = new int[len];
for (int i = 1; i < len; i++) {
parent[i] = i;
size[i] = 1;
}
}
public int index(int i, int j) {
return i * col + j + 1;
}
private int find(int i) {
int hi = 0;
while (i != parent[i]) {
help[hi++] = i;
i = parent[i];
}
for (int index = 0; index < hi; index++) {
parent[help[index]] = i;
}
return i;
}
public void union(int p1, int i, int j) {
int p2 = index(i, j);
int f1 = find(p1);
int f2 = find(p2);
if (f1 != f2) {
int s1 = size[f1];
int s2 = size[f2];
if (s1 >= s2) {
size[f1] += s2;
parent[f2] = f1;
} else {
size[f2] += s1;
parent[f1] = f2;
}
}
}
public boolean isSameSet(int p, int i, int j) {
return find(p) == find(index(i, j));
}
}
類似問題:LintCode 477. 被圍繞的區域
LintCode 178. 圖是否是樹
本題比較簡單,判斷條件如下:
-
如果
n
等於0,預設就是空樹,直接返回true
-
如果
n - 1 != 邊數量
,說明不是樹,因為對於有n
個點的樹,邊的數量一定是n-1
。 -
最重要的一個判斷條件:如果一個邊中的的兩個點的代表節點一樣,說明出現了環,所以,最後
union
掉所有邊的所有節點,如果集合個數不等於1,說明有環,直接返回false
。
完整程式碼見:
public static boolean validTree(int n, int[][] edges) {
if (n == 0) {
return true;
}
if (n - 1 != edges.length) {
return false;
}
LeetCode_0261_GraphValidTree.UnionFind uf = new LeetCode_0261_GraphValidTree.UnionFind(n);
for (int[] edge : edges) {
uf.union(edge[0], edge[1]);
}
return uf.setSize() == 1;
}
// 如何判斷環? 如果一個node節點中兩個點的代表點一樣,說明出現了環,直接返回false
public static class UnionFind {
private int[] parents;
private int[] size;
private int[] help;
private int sets;
public UnionFind(int n) {
parents = new int[n];
size = new int[n];
help = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
size[i] = 1;
}
sets = n;
}
public int find(int i) {
int hi = 0;
while (i != parents[i]) {
help[hi++] = i;
i = parents[i];
}
for (int j = 0; j < hi; j++) {
parents[help[j]] = i;
}
return i;
}
public void union(int i, int j) {
int f1 = find(i);
int f2 = find(j);
if (f1 != f2) {
int s1 = size[f1];
int s2 = size[f2];
if (s1 < s2) {
parents[f1] = parents[f2];
size[f2] += s1;
} else {
parents[f2] = parents[f1];
size[f1] += s2;
}
sets--;
}
}
public int setSize() {
return sets;
}
}
類似問題:LeetCode 261. 以圖判斷樹
LeetCode 952. 按公因數計算最大元件大小
本題關鍵解法也是並查集,只不過在union
過程中,需要將當前位置的所有因數得到並先儲存起來,我們可以用雜湊表來儲存這樣的關係,以6個數為例,存在雜湊表中有兩條記錄,即:
記錄1: key:3,value:6
記錄2: key: 2,value:6
當我來到下一個數的時候,如果這個數的因數有3,則我可以通過雜湊表直接找到曾經有一個6和你有共同的因數。然後就可以把這個數和6進行union
操作,最後只要返回並查集中集合元素最多的那個集合數量即可。完整程式碼如下:
public static int largestComponentSize(int[] arr) {
UnionFind uf = new UnionFind(arr.length);
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
// 以下是找arr[i]有哪些因數的相對比較快的做法。
int num = (int) Math.sqrt(arr[i]);
for (int j = 1; j <= num; j++) {
if (arr[i] % j == 0) {
if (j != 1) {
if (!map.containsKey(j)) {
map.put(j, i);
} else {
// 找到有共同因數的元素了,可以合併了
uf.union(map.get(j), i);
}
}
int other = arr[i] / j;
if (other != 1) {
if (!map.containsKey(other)) {
map.put(other, i);
} else {
// 找到有共同因數的元素了,可以合併了
uf.union(map.get(other), i);
}
}
}
}
}
return uf.maxSize();
}
// 並查集
public static class UnionFind {
private int[] parents;
private int[] size;
private int[] help;
public UnionFind(int len) {
parents = new int[len];
size = new int[len];
help = new int[len];
for (int i = 0; i < len; i++) {
parents[i] = i;
size[i] = 1;
}
}
public int maxSize() {
int ans = 0;
for (int size : size) {
ans = Math.max(ans, size);
}
return ans;
}
private int find(int i) {
int hi = 0;
while (i != parents[i]) {
help[hi++] = i;
i = parents[i];
}
for (int j = 0; j < hi; j++) {
parents[help[j]] = i;
}
return i;
}
// i 和 j分別是兩個數的位置,不是值
public void union(int i, int j) {
int f1 = find(i);
int f2 = find(j);
if (f1 != f2) {
int s1 = size[f1];
int s2 = size[f2];
if (s1 > s2) {
parents[f2] = f1;
size[f1] += s2;
} else {
parents[f1] = f2;
size[f2] += s1;
}
}
}
}