day 25-26 算法:並查集、島嶼的個數、朋友圈問題

兩個題解法其實是一致的,當多練一遍

1. 題目

  1. 給定一個由’1’(陸地)和’0’(水)組成的二維網格,計算島嶼的數量。一個島被水包圍,並且它是通過水平方向或者垂直方向相鄰的陸地連接而成。你可以假設網格的四個邊均被水包圍。https://leetcode-cn.com/problems/number-of-islands/
  2. 班上有N名學生,其中有些人是朋友,有些則不是。他們的友誼具有傳遞性。如果已知A是B的朋友,B是C的朋友,那麼我們可以認爲A也是C的朋友。所謂的朋友圈就是朋友的集合。https://leetcode-cn.com/problems/friend-circles/

2. 基本知識

2.1 並查集

  1. 定義

    英文是(union&find)是一種樹形的數據結構,用於處理一些不交集的合併和查詢問題

  2. find

    確定元素屬於哪一個子集,它可以被用來確定兩個元素是否屬於同一個子集

  3. union

    將兩個子集合併成一個集合

2.2 生活中的例子

  1. 小弟->老大(黑幫人員的從屬關係)

2.3 並查集的兩種優化

  1. 路徑壓縮

    尋找根節點,如果元素過多,就會比較複雜,樹形層次太多。路徑壓縮就是:把祖先節點作爲所有子孫節點的parent,這樣祖先節點和所有子孫節點都是直連的,find操作就變成O(1)的時間複雜度。

  2. 按秩合併(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的值即島嶼的個數。

  1. 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);
     }
    
  2. 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’的節點都指向自己

  2. 把相鄰的’1’進行合併

  3. 遍歷查看有多少個不同的集合

     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 染色法

  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);
     }
    
  2. 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的節點初始化

  2. 遍歷,如果當前節點爲1,則將周邊的所有1都合併到一個集合

  3. 得出總的集合數

     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;
     }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章