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