兩個題解法其實是一致的,當多練一遍
1. 題目
- 給定一個由’1’(陸地)和’0’(水)組成的二維網格,計算島嶼的數量。一個島被水包圍,並且它是通過水平方向或者垂直方向相鄰的陸地連接而成。你可以假設網格的四個邊均被水包圍。https://leetcode-cn.com/problems/number-of-islands/
- 班上有N名學生,其中有些人是朋友,有些則不是。他們的友誼具有傳遞性。如果已知A是B的朋友,B是C的朋友,那麼我們可以認爲A也是C的朋友。所謂的朋友圈就是朋友的集合。https://leetcode-cn.com/problems/friend-circles/
2. 基本知識
2.1 並查集
-
定義
英文是(union&find)是一種樹形的數據結構,用於處理一些不交集的合併和查詢問題
-
find
確定元素屬於哪一個子集,它可以被用來確定兩個元素是否屬於同一個子集
-
union
將兩個子集合併成一個集合
2.2 生活中的例子
- 小弟->老大(黑幫人員的從屬關係)
2.3 並查集的兩種優化
-
路徑壓縮
尋找根節點,如果元素過多,就會比較複雜,樹形層次太多。路徑壓縮就是:把祖先節點作爲所有子孫節點的parent,這樣祖先節點和所有子孫節點都是直連的,find操作就變成O(1)的時間複雜度。
-
按秩合併(rank合併)
秩表示樹的高度,在合併的時候,總是將較小秩的樹根指向較大的樹根,這樣合併後的數秩就不會變大。
2.4 並查集的代碼實現
public class QuickUnionF {
private int[] roots;
/**
* 構造新的並查集
* @param N 並查集的長度
*/
public QuickUnionF(int N) {
this.roots = new int[N];
for (int i = 0; i < N; i++) {
// 表示自己指向自己
roots[i] = i;
}
}
/**
* 查看元素屬於哪個集合,路徑壓縮版
* @param element 元素
* @return 元素所在集合
*/
public int find(int element) {
// 先找到字集所在的集合
int root = element;
// 只要節點沒指向自己,繼續遍歷,直接找到所屬集合爲止
while (root != roots[root]) {
//把當前的所屬集合取出來
root = roots[element];
}
//進行路徑壓縮
while(root != roots[element]) {
roots[element] = root;
}
return roots[element];
}
/**
* 判斷兩個元素是否在同一個集合
*/
public boolean isConnectd(int firstElement, int secondElement) {
return find(firstElement) == find(secondElement);
}
/**
* 兩個字集合合併
*/
public void unionElements(int firstElement, int secondElement) {
int firstUnion = find(firstElement);
int secondUnion = find(secondElement);
if (firstUnion != secondUnion) {
roots[firstUnion] = secondUnion;
}
}
3. 算法題解
3.1 給定一個由’1’(陸地)和’0’(水)組成的二維網格,計算島嶼的數量。
一個島被水包圍,並且它是通過水平方向或者垂直方向相鄰的陸地連接而成。你可以假設網格的四個邊均被水包圍。
示例:
11000
11000
00100
00011
輸出:3
3.1.1 解法1:染色法(Flood fill)
遍歷所有節點,如果節點爲1,count++,然後將周圍的是1的節點染色改爲0,最後得到count的值即島嶼的個數。
-
DFS
public static int numsIsLands(char[][] grid) { int x = grid.length; int y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { // count++ count++; // 將周邊的1給換成0 dfsFloodFill(grid, i, j); } } } return count; } private static void dfsFloodFill(char[][] grid, int i, int j) { int x = grid.length; int y = grid[0].length; //邊界及0不處理 if (i < 0 || j < 0 || i >=x || j >= y || grid[i][j] == '0') return; // 染色爲'0' grid[i][j] = '0'; dfsFloodFill(grid, i-1, j); dfsFloodFill(grid, i, j-1); dfsFloodFill(grid, i+1,j); dfsFloodFill(grid, i, j+1); }
-
BFS
遍歷邏輯跟DFS一樣,染色的邏輯使用的廣度優先搜索,藉助LinkedList數據結構,將相鄰節點放到暫存list中,挨個取出進行判斷,直到周邊節點爲0爲止。
public static int numsIsLands1(char[][] grid) { int x = grid.length; int y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { // count++ count++; // 將周邊的1給換成0 bfsFloodFill(grid, i, j); } } } return count; } private static void bfsFloodFill(char[][] grid, int i, int j) { LinkedList<int[]> chars = new LinkedList<>(); int x = grid.length; int y = grid[0].length; chars.add(new int[]{i, j}); while (!chars.isEmpty()) { int[] cur = chars.remove(); int a = cur[0]; int b = cur[1]; if (a < 0 || b < 0 || a >= x || b >= y || grid[a][b] == '0') continue; grid[a][b] = '0'; chars.add(new int[]{a - 1, b}); chars.add(new int[]{a + 1, b}); chars.add(new int[]{a, b - 1}); chars.add(new int[]{a, b + 1}); } }
3.1.2 解法2:並查集
-
初始化並查集,把所有爲’1’的節點都指向自己
-
把相鄰的’1’進行合併
-
遍歷查看有多少個不同的集合
class UnionFind { int count; int[] parent; int[] rank; public UnionFind(char[][] grid) { count = 0; int x = grid.length; int y = grid[0].length; parent = new int[x * y]; rank = new int[x * y]; // 初始化並查集 for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { int index = i * x + j; if (grid[i][j] == '1') { parent[index] = index; count++; } rank[index] = 0; } } } public int find(int index) { if (index != parent[index]) parent[index] = find(parent[index]); return parent[index]; } public void union(int x, int y) { int rootX = find(x); int rootY = find(y); // rank合併優化 if (rootX != rootY) { if (rank[rootX] > rank[rootY]) { parent[rootY] = rootX; } if (rank[rootX] < rank[rootY]) { parent[rootX] = rootY; } if (rank[rootX] == rank[rootY]) { parent[rootX] = rootY; rank[rootY] += 1; } --count; } } public int getCount() { return count; } } public int numsIsLands(char[][] grid) { if (grid == null || grid.length == 0) return 0; int x = grid.length; int y = grid[0].length; // 1. 初始化並查集 UnionFind unionFind = new UnionFind(grid); // 2. 遍歷合併爲1的節點 for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == '1') { grid[i][j] = '0'; // 合併上下左右的爲1的節點(上下是指座標的上下) // 合併上一個節點 if (j - 1 >= 0 && grid[i][j - 1] == '1') { unionFind.union(i * x + j, i * x + j - 1); } // 合併下一個節點 if (j + 1 < y && grid[i][j + 1] == '1') { unionFind.union(i * x + j, i * x + j + 1); } // 合併左邊的節點 if (i - 1 >= 0 && grid[i - 1][j] == '1') { unionFind.union(i * x + j, (i - 1) * x + j); } //合併右邊的節點 if (i + 1 < x && grid[i + 1][j] =='1') { unionFind.union(i * x + j, (i + 1) * x + j); } } } } // 3. 獲取不同的集合個數 return unionFind.getCount(); }
3.2 班上有N名學生,其中有些人是朋友,有些則不是。他們的友誼具有傳遞性。如果已知A是B的朋友,B是C的朋友,那麼我們可以認爲A也是C的朋友。所謂的朋友圈就是朋友的集合。
給定一個N*N的矩陣M,表示班級中學生之間的朋友關係。如果M[i][j]=1,表示已知第i個和第j個學生之間互爲朋友關係,否則爲不知道。你必須輸出所有學生中的已知的朋友圈總數。
示例:
輸入:
[[1,1,0].
[1,1,0],
[0,0,1]]
輸出:2
3.2.1 染色法
-
dfs遍歷染色
int x; int y; public int numsFriendCycles(int[][] grid) { x = grid.length; y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == 1) { count++; //dfs染色 dfsFloodFill(grid, i, j); } } } return count; } private void dfsFloodFill(int[][] grid, int i, int j) { if (i < 0 || j < 0 || i >= x || j >=y || grid[i][j] == 0) return; grid[i][j] = 0; dfsFloodFill(grid, i - 1, j); dfsFloodFill(grid, i + 1, j); dfsFloodFill(grid, i, j - 1); dfsFloodFill(grid, i, j + 1); }
-
bfs
int x; int y; public int numsFriendCycles1(int[][] grid) { x = grid.length; y = grid[0].length; int count = 0; for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { if (grid[i][j] == 1) { count++; //bfs染色 bfsFloodFill(grid, i, j); } } } return count; } // 這裏面的i,j 和循環裏面的a和b不能混淆 private void bfsFloodFill(int[][] grid, int i, int j) { LinkedList<int[]> bfsList = new LinkedList<>(); bfsList.add(new int[]{i, j}); while (!bfsList.isEmpty()) { int[] item = bfsList.remove(); int a = item[0]; int b = item[1]; if (a < 0 || b < 0 || a >= x || b >=y || grid[a][b] == 0) continue; grid[a][b] = 0; // 將周邊節點加入輔助Queue bfsList.add(new int[]{a-1, b}); bfsList.add(new int[]{a + 1, b}); bfsList.add(new int[]{a, b - 1}); bfsList.add(new int[] {a, b + 1}); } }
3.2.2 並查集
經過分析,可以看做一個並查集的問題,自己指向自己,然後合併相鄰的節點,最後看有多少個集合即可。
-
先將所有1的節點初始化
-
遍歷,如果當前節點爲1,則將周邊的所有1都合併到一個集合
-
得出總的集合數
class FriendUnionF{ int parent[]; int rank[]; int count = 0; public FriendUnionF(int[][] grid) { 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++) { int index = i * m + j; if (grid[i][j] == 1) { parent[index] = index; count ++; } rank[index] = 0; } } } public int find(int m) { if (parent[m] != m) { parent[m] = find(parent[m]); } return parent[m]; } // 合併時採用按rank合併優化 public void union(int m, int n) { // 分別查找root int rootM = find(m); int rootN = find(n); if (rootM != rootN) { if (rank[rootM] > rank[rootN]) { parent[rootN] = rootM; }else if (rank[rootM] < rank[rootN]) { parent[rootM] = rootN; }else{ parent[rootN] = rootM; rank[rootM] += 1; } --count; } } } public int numsFriendCycles(int[][] grid) { // 1. 先將所有1的節點初始化 FriendUnionF friendUnionF = new FriendUnionF(grid); int m = grid.length; int n = grid[0].length; // 遍歷,如果當前節點爲1,則將周邊的所有1都合併到一個集合 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) { grid[i][j] = 0; int index = i * m + j; //將周邊的1合併 if (i - 1 >= 0 && grid[i - 1][j] == 1) { // 注意第二個參數簡化的時候不要出錯,第一遍就因爲這個參數簡化導致出現問題 friendUnionF.union(index, (i - 1) * m + j); } if (i + 1 < m && grid[i + 1][j] == 1) { friendUnionF.union(index, (i + 1) * m + j); } if (j - 1 >= 0 && grid[i][j - 1] == 1) { friendUnionF.union(index, i * m + j - 1); } if (j + 1< n && grid[i][j + 1] == 1) { friendUnionF.union(index, i * m + j + 1); } } } } return friendUnionF.count; }