算法24:LeetCode_并查集相关算法(力扣547、200、305题)

目录

题目一:力扣547题,求省份数量

题目二:岛屿数量

题目三:岛屿数量拓展


什么是并查集,举个简单的例子。学生考试通常会以60分为及格分数,我们将60分及以上的人归类为及格学生,而60分以下归类为不及格的学生, 通过记录学生ID。现在要求根据学生的ID,迅速的找出这名学生成绩是否及格。这种归类就是合并,而根据学生ID查找就是查询,合起来就是并查集。

可能会有人说不用并查集也可以干这件事情,但是有没有想过一个问题,一个班级,一个年级,一个市,一个省,全中国。如果都要统计这些信息呢?如果要统计及格的全部人数呢?难到要一个一个去查吗?此处,并查集就发挥作用了。

下面推荐一篇博客,对并查集的解释还算通俗易懂,有兴趣的朋友可以看看https://blog.csdn.net/LWR_Shadow/article/details/124873281

下面来分享一些并查集的算法题:

题目一:力扣547题,求省份数量

题目的具体信息可以直接查看547. 省份数量

这是一道奇葩的题目,非此即彼,只要不相连的城市,就属于其他省。而现实中,比如苏州和无锡相连接,徐州和他们都不相连,但是无锡、苏州、徐州却都属于江苏省。既然题目是这么要求的,那我们就按照要求进行设计。

式例1:这组二维数组是什么意思呢?

从1节点的角度:【1,1,0】代表1节点自己连接自己,自己连接2节点,不连接点3节点。

从2节点的角度:【1,1,0】自己连接1节点,自己连接自己,自己不连接3节点。

从3节点的角度:【0,0,1】自己不连接1节点,不连接2节点,自己连接自己。

所以,得出的结论是节点1和节点2是同一个省,而3节点是另外一个省的城市,共2个省份

式例2:完全按照上方分析的思路去分析,节点1、节点2、节点3互不连接,也就是说他们分别属于不同的省份,这个demo共有3个省份。

下面使用并查集的知识进行解答:

package code03.并查集_04;

/**
 *
 * 链接 https://leetcode.cn/problems/number-of-provinces/
 */
public class Code01_ProvinceCount {

    public static int findCircleNum(int[][] M)
    {
        int length = M.length;
        UnionFind uom = new UnionFind(length);
        for (int i = 0; i < M.length; i++) {
            for (int j = i + 1; j < length; j++) {
                if (M[i][j] == 1) {
                    uom.union(i, j);
                }
            }
        }

        return uom.size();
    }

    static class UnionFind {
        //父节点
        private int[] parent;
        // 辅助结构
        private int[] help;
        // 一共有多少个集合
        private int sets;
        // i所在的集合大小是多少
        private int[] size;

        UnionFind(int length)
        {
            parent = new int[length];
            size = new int[length];
            //help的初始化, 个人想每次调用的时候初始化,但是数据量较大的
            //时候可能会吃内存
            help = new int[length];
            //本题比较特殊,二维数组长度为多少,集合最多就可能是多少
            sets = length;
            //记录数组的下标地址,可以通过下标找到父亲节点
            for (int i = 0; i < length; i++) {
                parent[i] = i;
                //i所在的集合大小是多少, 默认是自己, 所以是1
                size[i] = 1;
            }
        }

        public void union(int i, int j)
        {
            //获取到的根节点索引. 需要注意的是,第一次调用这i和j,返回的是
            //他们本身的索引值。不会涉及到里面的while和for。 这样,我们本
            //方法体才能安稳的做合并操作,才会存在下挂的后代节点。
            int indexI = findRoot(i);
            int indexJ = findRoot(j);

            //如果他们两个值不相等,说明他们2个
            //还没有成为同一类数据。因此,我们需要
            //把他们设置成同一类数据
            if (indexI != indexJ)
            {
                if (size[indexI] >= size[indexJ]) {
                    /**
                     * 此处的合并是合并2个不同的节点,将较小的节点指针指向较大的节点,
                     * 这样就实现了并查集合并的目的.这是功能性合并
                     *
                     * 而在findRoot方法中,合并的是同一节点的父节点,起到的是一个性能优化的作用
                     */
                    //等价于parent[indexJ] = parent[indexI], 因为没有合并之前,indexI == parent[indexI]
                    parent[indexJ] = indexI;
                    //更新合并后的根节点的后代数量
                    size[indexI] = size[indexI] + size[indexJ];
                    //被合并后的根节点,不再保存后代信息
                    size[indexJ] = 0;
                }
                else {
                    parent[indexI] = indexJ;
                    size[indexJ] = size[indexJ] + size[indexI];
                    size[indexI] = 0;
                }

                //因为我们默认的是有几组数据,就有几个省份。 但是此处发生了合并,也就意味着2组数据中
                //他们是在相同的省份中,因此默认值需要减少1.
                sets--;
            }
        }

        public int size() {
            return sets;
        }

        public int findRoot (int addressIndex)
        {
           int index = 0;
            /**
             *  第一次肯定是可以找到地址的,因为每个城市的父节点都是自己,
             *  所以他们都是在parent数组中的
             *
             *  但是, 经过合并后,我们只会保存合并后的父节点的地址下标。
             *  数组的形式,我们只是在parent数组中,更新当前城市的父节点
             *  下标地址。因此,以下的while循环就出出现了
             */
           while (addressIndex != parent[addressIndex]) {
                //记录下每一次遍历的父节点的下标,有可能有很多
                help[index] = parent[addressIndex];
                //指针指向父节点的下标,这样我们就可以逐层
                //网上找到最顶层的根节点了。
                addressIndex = parent[addressIndex];
                index++;  //index是比实际找的次数多1的
           }

            /**
             *  我们只是在parent数组中,更新当前城市的父节点下标地址
             *  这样我就达到了并查集,合并同类数据的功能
             *
             *  index是比实际找的次数多1的, 所以一开始就需要减1
             *  这也就不用担心help里面可能存在的脏数据问题了。
             */
           for (index--; index >=0; index--) {
               /**
                * 路径压缩,把之前每一次找到的父节点下标全部指向了根节点,
                * 这样以后再找的话就会减少上面的while循环次数了。
                * 因为我们判断是否是同一类数据,就是根据根节点的下标进行判断的
                *
                * 比如,a 和 b的根节点相同,那么我就可以认为a和b是同一类数据
                *
                * help数组之前记录了parent数组父节点的下标,因此需要根据
                * 下标把这些值都给改成根节点的下标,这样这些节点以后就全部
                * 指向根节点了
                */
               parent[help[index]] = addressIndex;
           }

           //其实,返回的就是一个父节点的地址下标值
           return addressIndex;
        }
    }

    public static void main(String[] args) {
        int[][] isConnected = {{1,1,0},{1,1,0},{0,0,1}};
        int size = findCircleNum(isConnected);
        System.out.println(size); //预期输出2

        int[][] isConnected2 =  {{1,0,0},{0,1,0},{0,0,1}};
        int size2 = findCircleNum(isConnected2);
        System.out.println(size2); //预期输出2
    }
}

题目二:岛屿数量

原题可以直接查看200. 岛屿数量

 这一题和上一题省份数量有相同和不同的部分:

相同部分是1和1相连接,就属于1片岛屿,还是算1个岛,这点和省份计算是一样的。

不同的部分是,省份中0也代表一个城市,只是它和其他城市不相连;而这道题中,0代表的是水,不是岛屿,因此在初始化的时候是有区别的。

并查集方式实现:

package code03.并查集_04;

/**
 * https://leetcode.com/problems/number-of-islands/
 * 并查集方式实现
 */
public class Code02_NumberOfLands {

    public int numIslands(char[][] grid)
    {
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int rowLength = grid.length;
        int colLength = grid[0].length;

        UnionFind uf = new UnionFind(grid);
        //合并第一行
        for (int j = 1; j < colLength; j++) {
            if (grid[0][j - 1] == '1' && grid[0][j] == '1') {
                uf.union(0, j - 1, 0, j);
            }
        }
        //合并第一列
        for (int i = 1; i < rowLength; i++) {
            if (grid[i - 1][0] == '1' && grid[i][0] == '1') {
                uf.union(i - 1, 0, i, 0);
            }
        }

        //从第二行第二列开始遍历
        for (int i = 1; i < rowLength; i++) {
            for (int j = 1; j < colLength; j++) {
                if (grid[i][j] == '1') {
                    //上一行合并
                    if (grid[i][j - 1] == '1') {
                        uf.union(i, j - 1, i, j);
                    }
                    //前一列合并
                    if (grid[i - 1][j] == '1') {
                        uf.union(i - 1, j, i, j);
                    }
                }
            }
        }

        return uf.sets;
    }

    static class UnionFind
    {
        int[] parents;
        int[] size;
        int sets;
        int[] helps;
        int row;
        int col;

        public UnionFind(char[][] gg)
        {
            row = gg.length;
            col = gg[0].length;
            int length = row * col;

            parents = new int[length];
            size = new int[length];
            helps = new int[length];
            sets = 0;

            for (int i = 0; i < row; i++) {
                for (int j = 0; j < col; j++) {
                    //优化,只有是1的是,才会更新parent下标为自己
                    if (gg[i][j] == '1') {
                        //生成唯一地址
                        int index = index(i, j);
                        //默认自己就是自己的父亲节点
                        parents[index] = index;
                        //每个父节点下挂的节点数量,默认为1
                        size[index] = 1;
                        //默认每出现1都是一个岛屿
                        sets++;
                    }
                }
            }
        }

        public void union (int row1, int col1, int row2, int col2)
        {
            //父节点
            int parentIndex1 = index(row1, col1);
            int parentIndex2 = index(row2, col2);
            //根节点
            int rootIndex1 = findRoot(parentIndex1);
            int rootIndex2 = findRoot(parentIndex2);

            //根节点不同,则合并
            if (rootIndex1 != rootIndex2) {
                //将小的挂在大的下面
                if (size[rootIndex1] >= size[rootIndex2]) {
                    parents[rootIndex2] = rootIndex1;
                    size[rootIndex1] = size[rootIndex1] + size[rootIndex2];
                    size[rootIndex2] = 0;
                }
                else {
                    parents[rootIndex1] = rootIndex2;
                    size[rootIndex2] = size[rootIndex1] + size[rootIndex2];
                    size[rootIndex1] = 0;
                }
                sets--;
            }
        }

        public int index (int r, int c) {
            //列的长度是固定的col
            return r * col + c;
        }

        public int findRoot (int index)
        {
            int rootIndex = 0;
            //并查集之前合并过
            while (index != parents[index]) {
                //记录下每一次找到的上层节点(父节点)
                helps[rootIndex] = parents[index];
                //当前地址指向上层节点(父节点)
                index = parents[index];
                rootIndex++;
            }

            //路径压缩
            for (rootIndex--; rootIndex > 0; rootIndex--) {
                //返回原始收集的上层节点地址下标
                int t = helps[rootIndex];
                //根据下标,更新到根节点地址,
                //这样所以的地址都指向了根点处,优化了性能
                parents[t] = index;
            }
            return index;
        }
    }




    public static void main(String[] args) {

        char[][] bb = {{'1','1','1','1','0'},{'1','1','0','1','0'},{'1','1','0','0','0'},{'0','0','0','0','0'}};

        Code02_NumberOfLands tt = new Code02_NumberOfLands();
        int num = tt.numIslands(bb);
        System.out.println(num);
    }
}

渲染方式实现:

package code03.并查集_04;

/**
 * 感染方式实现,性能非常高
 * 局限是部分案例无法解决
 */
public class Code02_NumberOfLands_extension {

    public int numIslands(char[][] grid)
    {
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int num = 0;
        for (int row = 0; row < grid.length; row++) {
            for (int col = 0; col < grid[row].length; col++) {
                if (grid[row][col] == '1') {
                    num++;
                    infect(grid, row, col);
                }
            }
        }

        return num;
    }

    public void infect (char[][] bb, int row, int col)
    {
        if (row < 0 || row == bb.length
                || col < 0 || col == bb[0].length
                || bb[row][col] != '1') {
            return;
        }
        bb[row][col] = 0;
        //上
        infect(bb, row-1, col);
        //下
        infect(bb, row+1, col);
        //左
        infect(bb, row, col-1);
        //右
        infect(bb, row, col+1);
    }

    public static void main(String[] args) {

        char[][] bb = {{'1','1','1','1','0'},{'1','1','0','1','0'},{'1','1','0','0','0'},{'0','0','0','0','0'}};

        Code02_NumberOfLands_extension tt = new Code02_NumberOfLands_extension();
        int num = tt.numIslands(bb);
        System.out.println(num);
    }
}

这一道题,渲染的方式是最优解,它的性能是高于并查集实现方式的。但是,并查集的方式可以解决很多渲染方式无法解决的问题。 因此,渲染方式和并查集方式,我们都要掌握。

题目三:岛屿数量拓展

这是一道收费题:https://leetcode.com/problems/number-of-islands-ii/

* 题目:
* 设定一个二维数组,行为 m 列为 n.
* 现在给你一组地标数据,可以定位二维数组的具体位置。 每个地标都代表有1个岛屿,
* 但是如果连在一起的话只能算做一个岛。要求每次空降一次数据,求每次的岛屿数量。
*
* 假设 3 行 3列的 二维数组。
* 给定的坐标为:[[0,0],[0,1],[1,2],[2,,1]]
* 【0,0】位置确定,此时岛为1
* 【0,1】位置确定,此时岛为1
* 【1,2】位置确定,此时岛为2
* 【2,1】位置确认,此时岛为3
*
* 输出的结果为: 【1,1,2,3】
* 请设计一种算法
package code03.并查集_04;

import java.util.ArrayList;
import java.util.List;

/**
 * https://leetcode.com/problems/number-of-islands-ii/
 *
 * 题目:
 * 设定一个二维数组,行为 m 列为 n.
 * 现在给你一组地标数据,可以定位二维数组的具体位置。 每组数据都代表有1个岛屿,
 * 但是如果连在一起的话只能算做一个岛。要求每次空降一次数据,求每次的岛屿数量。
 *
 * 假设 3 行 3列的 二维数组。
 * 给定的坐标为:[[0,0],[0,1],[1,2],[2,,1]]
 * 【0,0】位置确定,岛为1
 * 【0,1】位置确定,岛为1
 * 【1,2】位置确定,岛为2
 * 【2,1】位置确认,岛为3
 *
 * 输入的结果为: 【1,1,2,3】
 *  请设计一种算法
 */
public class Code03_NunbOfLandsII {

    public List<Integer> numIslands(int m, int n, int[][] positions)
    {
        List<Integer> list = new ArrayList<>();
        if (m < 0 || n < 0 ||
                positions == null || positions.length == 0
                || positions[0].length == 0) {
            return list;
        }

        //此时的初始化内部不同于之前的初始化
        UnionFind uf = new UnionFind(m, n);
        for (int[] position : positions) {
            list.add(uf.connect(position[0], position[1]));
        }
        return list;
    }

    static class UnionFind
    {
        private int[] parents;
        private int[] size;
        private int[] helps;
        private int sets;
        private int col;
        private int row;


        public UnionFind(int m, int n) {

            int length = m * n;
            parents = new int[length];
            size = new int[length];
            helps = new int[length];
            sets = 0;
            row = m;
            col = n;

        }

        public int connect (int row, int col)
        {
            //获取当前位置
            int curPosition = index(row, col);
            //判断空间的位置是否已经是岛屿,默认为0
            if (size[curPosition] == 0 ) {
                size[curPosition] = 1;
                parents[curPosition] = curPosition;
                sets++;

                //合并,和渲染解题思路有点相似
                union(row, col, row-1, col); //上一行
                union(row, col, row+1, col); //下一行
                union(row, col, row, col-1);  //前一列
                union(row, col, row, col+1);  //后一列
            }

            return sets;
        }

        public void union (int row1,int col1, int row2, int col2)
        {
            //越界,无法合并
            if (row1 < 0 || row2 < 0 || col1 < 0 || col2 < 0
                    || row1 == row || row2 == row
                    || col1 == col || col2 == col) {
                return;
            }
            //父节点
            int parentIndex1 = index(row1, col1);
            int parentIndex2 = index(row2, col2);

            //如果2个中不全是岛屿,则不合并
            //需要注意的地方,写忘记了, debug才查出问题
            if (size[parentIndex1] == 0 || size[parentIndex2] == 0) {
                return;
            }
            //根节点
            int rootIndex1 = findRoot(parentIndex1);
            int rootIndex2 = findRoot(parentIndex2);

            //根节点不同,则合并
            if (rootIndex1 != rootIndex2) {
                //将小的挂在大的下面
                if (size[rootIndex1] >= size[rootIndex2]) {
                    parents[rootIndex2] = rootIndex1;
                    size[rootIndex1] = size[rootIndex1] + size[rootIndex2];
                }
                else {
                    parents[rootIndex1] = rootIndex2;
                    size[rootIndex2] = size[rootIndex1] + size[rootIndex2];
                }
                sets--;
            }
        }

        public int index (int r, int c) {
          return r * col + c;
        }

        public int findRoot (int index)
        {
            int rootIndex = 0;
            //并查集之前合并过
            while (index != parents[index]) {
                //记录下每一次找到的上层节点(父节点)
                helps[rootIndex] = parents[index];
                //当前地址指向上层节点(父节点)
                index = parents[index];
                rootIndex++;
            }

            //路径压缩
            for (rootIndex--; rootIndex > 0; rootIndex--) {
                //返回原始收集的上层节点地址下标
                int t = helps[rootIndex];
                //根据下标,更新到根节点地址,
                //这样所以的地址都指向了根点处,优化了性能
                parents[t] = index;
            }
            return index;
        }
    }

    public static void main(String[] args) {
        int m = 3;
        int n = 3;
        int[][] positions = {{0,0},{0,1},{1,2},{2,1}};

        Code03_NunbOfLandsII tt = new Code03_NunbOfLandsII();
        List list = tt.numIslands(3, 3, positions);
        for(int i = 0; i < list.size(); i++) {
            System.out.println("第 " + (i+1) + " 次空降,岛屿数量为: " + list.get(i));
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值