自学内容网 自学内容网

【LeetCode】十七、并查集:岛屿数量

1、并查集Union Find

并查集,一种树形的数据结构,处理不相交的两个集合的合并、查询、连通性问题。

【参考:📕经典解释】

引用上面文章的经典解释并查集:

  • 一个集合就像一个江湖门派,他们有一个教主(代表元),集合中每个元素只需要记住自己的上级,而不是教主
  • find即查询某个元素的代表元(教主)
  • union即把两个门派(集合),合并成一个,认一个教主

在这里插入图片描述

例子:

在这里插入图片描述

查find(x)

有个数组array,长度为10,若array[1] = 6,说明1号元素的上级是6号元素。array[6] = 6,说明6号元素的上级是6号元素,即6号是教主。并查集的查:find(x),即找x元素的教主

public int find(int x ) {
// 如果x位置的值,等于x,说明x就是教主
if (x == array[x]) {
return array[x];
}
// 如果不是,查x位置的值的上级
return find(array[x]);
}

也可不用递归:

public int find(int x ) {
while(x != array[x]) {
// 查x位置的元素自己的上级
x = array[x];
}
return x;
}

并union(x,y)

两个集合合并,或者说两个门派合并,看似改动很大,其实只要让一个门派的教主给另一个门派的教主当上级即可。因为只关注两个集合的连通性。

// 传入两个门派的元素
public void union(int x, int y) {
//查教主
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 让Y门派的教主当X门派教主的上级
array[rootX] = rootY;
}
}

如下树形,

在这里插入图片描述

对应的数组:数组索引代表元素,数组的值,代表这个位置的元素的上级

在这里插入图片描述
在这里插入图片描述
合并后,比如让第一个集合的教主做合并后新集合的教主:5号的上级不再是自己,变成了1

在这里插入图片描述

2、并查集find的优化:路径压缩 Quick find

用开头引用文章的形象例子,现在要做的就是,防止树形结构过于深,导致查询效率变低,将结果压缩成,只要查一次,就能知道教主是谁:

在这里插入图片描述

总之一句话,防止树太高,期待右边,而不是左边

在这里插入图片描述

想实现这个效果,可以:查询到x元素的根节点(教主)后,直接将x元素的上级改成根节点。

public int QuickFind(int x ) {
// 如果x位置的值,等于x,说明x就是教主
if (x == array[x]) {
return array[x];
}
// 如果不是,查x位置的值的上级,等递归回退的时候,把x的上级改成根节点
return array[x] = find(array[x]);
}

也就是说,第一次find元素x,是没有压缩路径的,第二次才是优化后的效果

3、并查集union的优化:权重标记

和find优化属于同一思想,union两个集合时,防止树过高,比如合并:

在这里插入图片描述
如果这么连,树过高:

在这里插入图片描述
比较两棵树的高度,左边为2,右边为3,因此,高的树原地不动,继续做老大,低的树靠过去,union后的效果:

在这里插入图片描述

核心思想:

在这里插入图片描述

整体代码如下,带权重的union参考:

public class UnionFind {

const int  N=1005//指定并查集所能包含元素的个数(由题意决定)
int pre[N];     //存储每个结点的前驱结点的数组 
int rank[N];    //树的高度,权重

void init(int n) {     //初始化函数,对录入的 n个结点进行初始化 
    for(int i = 0; i < n; i++){
        pre[i] = i;     //每个结点的上级都是自己 
        rank[i] = 1;    //每个结点构成的树的高度为 1 
    } 
}

int find(int x) {
     //查找结点 x的根结点 
if (pre[x] == x) { 
return x ;//递归出口:x的上级为 x本身,则 x为根结点 
}  
    return find(pre[x]); //递归查找 
} 
 
int findQucik(int x) {     //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
    if (pre[x] == x) {
    return x;//递归出口:x的上级为 x本身,即 x为根结点 
    }
    return pre[x] = find(pre[x]);   //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
} 

bool isSame(int x, int y) {     //判断两个结点是否连通 
    return find(x) == find(y);  //判断两个结点的根结点(即代表元)是否相同 
}

void union(int x,int y) {
    int rootx = find(x);//寻找 x的代表元
    int rooty = find(y);//寻找 y的代表元
    if(rootx != rooty) {//如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并
    if(rank[rootx] > rank[rooty]) {//如果 x的高度大于 y,则令 y的上级为 x
    pre[rooty] = rootx;
    } else if (rank[rootx] < rank[rooty]) {
    pre[rootx] = rooty;//让 x的上级为 y
    } else {
    pre[rooty] = rootx;    
        rank[rootx] += 1;//如果 x的高度和 y的高度相同,则令 y的高度加1
}
}
}
}

4、leetcode200:岛屿数量

在这里插入图片描述

关于题目的理解,1为陆地,0为水,二维网格外都是水两个示例中的陆地如下:

在这里插入图片描述

解法一:DFS

以示例二来分析,从二维网格的第一个元素开始遍历,找到1后,将其改为0(同化),并将其水平或竖直方向上相邻的1也改为0,如下

在这里插入图片描述
递归,将找到的1 水平或竖直方向上相邻的1 水平或竖直方向上相邻的1 再次改为0,结果result + 1,如下:

在这里插入图片描述
继续往下遍历,找到了第二个1(说明这又是一块新的岛屿,因为前面同化时,没有把它变成0,即它和前一块岛屿不挨着) ,继续将其同化成0,并递归把其水平或竖直方向上相邻的1也全都改为0,结果result + 1:

在这里插入图片描述
继续往下遍历,找到了第三个1,又是一块新的岛屿,继续同化成0,递归把其水平或竖直方向上相邻的1也全都改为0,结果result + 1

在这里插入图片描述
继续遍历,没1了,结束,最终result为3。代码实现:

public class P200 {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }
        // 行数
        int row = grid.length;
        // 列数
        int col = grid[0].length;
        // 岛屿数量
        int result = 0;
        // 遍历二维网格
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == '1') {
                    // 遇到1,岛屿数量加一,并同化周边陆地
                    result = result + 1;
                    // 深度优先,改这个1水平竖直方向上的非0数值
                    dfs(grid, i, j, row, col);
                }
            }

        }
        return result;
    }

    public static void dfs(char[][] grid, int x, int y, int row, int col) {
        // 终止条件:出界或者水平、竖直方向上遇到了0(水域)
        if (x < 0 || y < 0 || x >= row || y >= col || grid[x][y] == '0') {
            return;
        }
        // 同化
        grid[x][y] = '0';
        // 上下左右四个方位
        dfs(grid, x + 1, y, row, col);
        dfs(grid, x - 1, y, row, col);
        dfs(grid, x, y + 1, row, col);
        dfs(grid, x, y - 1, row, col);

    }
}

DFS同化点(x, y)的上下左右四个位置的点:

在这里插入图片描述
核心是:找到1后,检查其上下左右四个点是否为1,再检查其上下左右的上下左右,直到出界或者遇到了水域

解法二:BFS

在这里插入图片描述
遍历二维网格,遇到1,入队,出队然后把其水平或竖直方向上相邻的1入队,并都同化成0,注意,不再像深度优先一样套娃(把1 水平或竖直方向上相邻的1 水平或竖直方向上相邻的1 再次改为0)。流程如下:

  • 遍历二维网格,找到第一个1,坐标(0,0),也就是图中左上角那个
    在这里插入图片描述

  • 将其入队并同化成0

  • 出队,并检查其水平或竖直方向上相邻的位置,有1的话,入队并同化成0
    在这里插入图片描述

  • 出队,并检查出队元素的水平或竖直方向上相邻的位置,有1的话,入队并同化成0

在这里插入图片描述

  • 继续出队,直到队列为空,则岛屿数量 + 1,继续往下遍历二维网格,找到下一个1

在这里插入图片描述

  • 入队并同化成0,继续往下

总之,总体思路和DFS一样,不同的是,同化其水平竖直方向上的1时,DFS深度优先,一次同化完,BFS则依靠队列的出队入队,每次只处理相邻的1。(DFS是处理相邻的相邻,连坐到都没1了)

public class P200 {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }
        // 行数
        int row = grid.length;
        // 列数
        int col = grid[0].length;
        // 岛屿数量
        int result = 0;
        // 队列
        Queue<int[]> queue = new LinkedList<>();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == '1') {
                    queue.add(new int[]{i, j});
                    grid[i][j] = '0';
                    while (!queue.isEmpty()) {
                        int[] point = queue.poll();
                        int x = point[0];
                        int y = point[1];
                        // 只看这个1的上下左右四个点,如果这四个点有1,则同化成0,并将这个点自身入队
                        // 队列不空时,while下一轮会处理新入队的点的上下左右四个点
                        // 如此,while执行完,和前面DFS一口气改完了这块岛屿所有的1是一个效果
                        if (x + 1 < row && grid[x + 1][y] == '1') {
                            grid[x + 1][y] = '0';
                            queue.add(new int[]{x + 1, y});
                        }
                        if (x - 1 >= 0 && grid[x - 1][y] == '1') {
                            grid[x - 1][y] = '0';
                            queue.add(new int[]{x - 1, y});
                        }
                        if (y + 1 < col && grid[x][y + 1] == '1') {
                            grid[x][y + 1] = '0';
                            queue.add(new int[]{x, y + 1});
                        }
                        if (y - 1 >= 0 && grid[x][y - 1] == '1') {
                            grid[x][y - 1] = '0';
                            queue.add(new int[]{x, y - 1});
                        }
                    }
                    // 发现的1的水平竖直方向相邻的1都同化成0了,岛屿数量+1
                    result = result + 1;
                    // 继续往下遍历二维网格,找下一个1
                }

            }
        }
        return result;
    }

}

解法三:并查集Union Find

这题,对岛屿的描述,是其水平或竖直方向上相邻的1即同一块岛屿,这就是个连通性。先看下二维数组和一维数组的转换:

在这里插入图片描述
设二维数组有元素[x][y],且总列数为col,则:

  • 一维数组索引index = x * col + y

  • x = index / col

  • y = index % col

将题中的二维数组转成一维数组:

在这里插入图片描述
根据并查集的步骤,先初始化数组,刚开始时,array[i] = i,即元素i的上级是i自己

在这里插入图片描述

接下来,遍历二维数组,找到1后,检查其上下左右四个点,是不是也是1,是则做Union,让他们有同一个上级,比如左上角(0,0)位置的那个1

在这里插入图片描述

union两次后,1和5的上级都变成了0,一维数组变为:

在这里插入图片描述
继续往下遍历,找1,第二个1为(0,1),继续检查其上下左右四个点,是不是也是1,是则做Union,如下:

在这里插入图片描述

union一次后,6的上级也变成了0,一维数组变为:

在这里插入图片描述
继续往下遍历,找到了第三个1,即第二行第一列那个1,但发现这个1已被union,其上级为0,因此,继续往下。

到第三行第三列的那个1时,其对应的一维数组坐标为12,上级为12,检查其上下左右四个点,发现没有1,因此,保持不变,继续往下走

在这里插入图片描述

继续往下走,到了最后一行的第四列,又发现1了,检查其上下左右四个点,发现右边有个1,做一次union

在这里插入图片描述

数组变为:

在这里插入图片描述

继续往下遍历,二维网格遍历结束,岛屿的数量 = 陆地数 - union的次数,或者说等于总数 - 水域数 - union的次数。(因为union一次,代表有一块陆地和其他岛屿连一起了),最后得出,岛屿数 = 20 - 13 - 2 - 1 - 1 = 3

public class P200 {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }
        // 行数
        int row = grid.length;
        // 列数
        int col = grid[0].length;
        // 水域数量初始值为0,以后每遇到一块水域,该值+1
        int waters = 0;
        UnionFind unionFind = new UnionFind(grid);
        // 遍历二维网格
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == '0') {
                    waters++;
                } else {
                    // 是陆地,检查其上下左右四个点,是1则union
                    if (i + 1 < row && grid[i + 1][j] == '1') {
                        // 注意这里二维坐标转为一维坐标
                        unionFind.union((i + 1) * col + j, i * col + j);
                    }
                    if (i - 1 >= 0 && grid[i - 1][j] == '1') {
                        unionFind.union((i - 1) * col + j, i * col + j);
                    }
                    if (j + 1 < col && grid[i][j + 1] == '1') {
                        unionFind.union(i * col + (j + 1), i * col + j);
                    }
                    if (j - 1 >= 0 && grid[i][j - 1] == '1') {
                        unionFind.union(i * col + (j - 1), i * col + j);
                    }

                }
            }
        }
        // count此时为岛屿数 + 水域数
        return unionFind.getCount() - waters;

    }
}

/**
 * 并查集
 */
class UnionFind {
    // 一维数组
    int[] root;
    // 用于计算岛屿的数量
    int count = 0;

    // 构造函数
    UnionFind(char[][] grid) {
        // 行数
        int row = grid.length;
        // 列数
        int col = grid[0].length;
        // 初始值为总数,即陆地 + 水域的数量,以后每union一次,count就减一
        count = row * col;
        root = new int[row * col];
        // 初始化
        for (int i = 0; i < root.length; i++) {
            root[i] = i;
        }
    }

    /**
     * find+压缩路径优化
     * 返回x的祖先
     */
    public int find(int x) {
        if (x == root[x]) {
            return x;
        } else {
            root[x] = find(root[x]);
            return root[x];
        }
    }

    /**
     * 合并
     */
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            root[rootX] = rootY;
            // union一次了,注意count减一
            count--;
        }
    }

    /**
     * 获取count的值
     * 和并查集的理论无关,纯给这道题加的
     */
    public int getCount() {
        return this.count;
    }
}

原文地址:https://blog.csdn.net/llg___/article/details/140378357

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!