Leetcode --- 并查集相关题目

写在前:并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(Union-find Algorithm)定义了两个用于此数据结构的操作:

  • Find(查找):确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • Union(合并):将两个子集合并成同一个集合。

ps:这里的集合也可叫连通块(连通分量),典型应用解决连通分量问题。

并查集解决单个问题(添加,合并,查找)的时间复杂度都是O(1),可以应用在在线算法中。

路径压缩算法:即查找过程,最理想的情况就是所有点的代表节点都是相同,一共就是两级结构。做法就是在查找时找到根之后把自身的值修改成根的下标,可以通过递归和迭代实现。

路径压缩

1.冗余连接(684 - 中)

题目描述:在本问题中, 树指的是一个连通且无环的无向图。输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

返回一条可以删去的边,使得结果图是一个有着N个节点的树如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。

示例 :

输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
    |   |
    4 - 3

思路:树是一个连通并无环的无向图,如果一棵树有 N个节点,则这棵树有 N-1条边。初始化N个节点,每个节点属于不同的集合。

可以通过并查集寻找附加的边,本质是检测图的环。如果边的两个节点已经出现在同一个集合里(在合并之前已经连通),说明着边的两个节点已经连在一起了,如果再加入这条边一定就出现环了,记录并返回

代码实现:

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length + 1;
        UnionFind uf = new UnionFind(n);
        for (int[] edge : edges) {
            uf.union(edge[0], edge[1]);
        }
        return uf.ans;
    }
}

class UnionFind {
    int[] roots;
    int[] ans = new int[2];  // 存放结果集

    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
    }

    public int find(int i) {
        if (i != roots[i]) {
            roots[i] = find(roots[i]);
        }
        return roots[i];
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
        } else {
            ans[0] = p;
            ans[1] = q;
        }
    }
}

2.省份数量(547 - 中)

题目描述:有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量(城市群)。

示例 :

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

思路:并查集解法(单独写一个并查集类)

  • 图的顶点数为 n,则初始化 n 个单顶点集合,每个集合指向自身。

  • 然后遍历图中的每个顶点,将当前顶点与其邻接点进行合并。

  • 最终结果返回合并后的集合的数量即可。

代码实现:

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        // 初始化n个单顶点集合,
        UnionFind uf = new UnionFind(n);
        // 遍历一半矩阵(两个城市単连接即可),遇到1连接两个城市
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (isConnected[i][j] == 1) {
                    uf.union(i, j);
                }
            }
        }
        return uf.size;
    }
}

class UnionFind {
    int[] roots;   // 记录每个省份的核心城市
    int size;   // 集合的数量

    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
        size = n;
    }
    // 寻找当前城市所在省份(城市群)的核心城市
    public int find(int i) {
        //若当前城市不是核心城市,则递归更新(类似树结构)
        if (i != roots[i]) {
            roots[i] = find(roots[i]);
        }
        return roots[i];
    }
    // 将两个城市连接到一个省份
    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        // 当两个城市的核心城市不同时,连接并更新核心城市,集合数量-1;若两个城市在同一省份则不减
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
            size--;
        }
    }
}

3.被围绕的区域(130 - 中)

题目描述:给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充,返回填充后的矩阵。

示例 :

输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

思路本题中O本质就是两大类:一种可以连通到边界,一种不能连通到边界(需要X覆盖)。

  • 并查集就是解决一种分类(连通性)问题的。对于每个节点我们用行数和列数生成id进行标识(用于下一步)。
  • 遍历每个O节点,上下左右四个方向进行合并:定义dummy节点区分上述两大分类(将连通到边界的与dummy进行合并)。
    • 如果当前O在边界,先与dummy节点合并
    • 否则将当前点与上下左右O进行合并
  • 再遍历矩阵,只需要判断O节点是否与dummy节点连通,如不连通则替换为X。

代码实现:

class Solution {
    int row, col;
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        row = board.length;
        col = board[0].length;
        int dummy = row * col;  // 虚拟分类点
        UnionFind uf = new UnionFind(row * col + 1);

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (board[i][j] == 'O') {
                    //当前节点在边界就和 dummy 合并
                    if (i == 0 || i == row - 1 || j == 0 || j == col - 1) {
                        uf.union(dummy, node(i, j));
                    } else {
                        //将上下左右的 O 节点和当前节点合并
                        if (board[i - 1][j] == 'O') uf.union(node(i, j), node(i - 1, j));
                        if (board[i][j - 1] == 'O') uf.union(node(i, j), node(i, j - 1));
                        if (board[i + 1][j] == 'O') uf.union(node(i, j), node(i + 1, j));
                        if (board[i][j + 1] == 'O') uf.union(node(i, j), node(i, j + 1));
                    }
                }
            }
        }

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (uf.isConnected(node(i, j), dummy)) {
                    board[i][j] = 'O';
                } else {
                    board[i][j] = 'X';
                }
            }
        }
    }
    
    int node(int i, int j) {
        return i * col + j;
    }
}

class UnionFind {
    int[] roots;

    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
    }
    
    public int find(int i) {
        while (i != roots[i]) {
            roots[i] = roots[roots[i]];
            i = roots[i];
        }
        return i;
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
        }
    }

    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }
}

ps:这里并查集的效率有点低。

4.岛屿的数量(200 - 中)

题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 :

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

思路:使用并查集计算集合(岛屿)的数量。对于陆地情况可以上下左右进行查找。类似547省份数量,用size变量保存连通分量的数量

注意:最终size = 连通块的个数 + 水的元素个数

  • 故在遍历每个元素的时候,统计水的个数(0的个数)
  • 岛屿数量 = size - 水的个数

代码实现:

class Solution {
    public int row, col;
    public int numIslands(char[][] grid) {
        row = grid.length;
        col = grid[0].length;
        int n = row * col;
        int ocean = 0;
        UnionFind uf = new UnionFind(n);

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == '0') {
                    ocean += 1;
                } else {
                    if (i > 0 && grid[i - 1][j] == '1') uf.union(node(i, j), node(i - 1, j));
                    if (j > 0 && grid[i][j - 1] == '1') uf.union(node(i, j), node(i, j - 1));
                    if (i < row - 1 && grid[i + 1][j] == '1') uf.union(node(i, j), node(i + 1, j));
                    if (j < col - 1 && grid[i][j + 1] == '1') uf.union(node(i, j), node(i, j + 1));
                }
            }
        }
        return uf.size - ocean;
    }
    int node (int i, int j) {
        return i * col + j;
    }
}

class UnionFind {
    int[] roots;
    int size;   // 岛屿数量

    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
        size = n;
    }

    public int find(int i) {
        while (i != roots[i]) {
            roots[i] =roots[roots[i]];
            i = roots[i];
        }
        return i;
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
            size--;
        }
    }
}

5.移除最多的同行或同列石头(947 - 中)

题目描述:如果想移除一个石头,那么它所在的行或者列必须有其他石头存在。我们能移除的最多石头数是多少?也就是说,只有一个石头在连通分量中,才能被移除。对于连通分量而言,最理想的状态是只剩一块石头。对于任何容量为n(n>1)一个连通分量,可以移除的石头数都为n-1。

示例 :

输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
输出:3
解释:一种移除 3 块石头的方法如下所示:
1. 移除石头 [2,2] ,因为它和 [2,0] 同行。
2. 移除石头 [2,0] ,因为它和 [0,0] 同列。
3. 移除石头 [0,2] ,因为它和 [0,0] 同行。
石头 [0,0] 和 [1,1] 不能移除,因为它们没有与另一块石头同行/列。

思路:本题注意理解我们要删除最多的石头(最后返回值),为此我们将所有的行和列进行连接呢?

  • 在图上构建若干个集合,每个集合可以看成一个钥匙串,最后每个钥匙串至少剩下一个钥匙。一个核心(尽可能多删除)
  • 并查集里的元素是 描述「横坐标」和「纵坐标」的数值
  • 假设有M个集合,设总结点数为N,那么显而易见,我们能够删除的总结点数,也即move数为:move = N - M;

在原始的输入中,行标号和列标号是共用的[1,n]区间内的数字,导致行值和列值重复,无法达到我们完成并查集的结点连接的目的。

  • 根据题目给出的数据范围,单个坐标数值小于10000。

  • 将列值映射到[10000,10000+n]的区间内,这样我们就获得了所有节点的标号。

代码实现:

class Solution {
    public int removeStones(int[][] stones) {
        int size = stones.length;
        // 题目给的数值在0 -10000,0-10000给横坐标,10001-20002留给纵坐标
        UnionFind uf = new UnionFind(20002);
        for (int i = 0; i < size; i++) {
            uf.union(stones[i][0], stones[i][1] + 10001);
        }

        // 统计集合个数(即roots数量),由于行列为同一roots,故统计一个即可
        HashSet<Integer> set = new HashSet<>();
        for (int i = 0; i < size; i++) {
            set.add(uf.find(stones[i][0]));
        }
        // 总石头数量 - roots数量 == 最多可以删除的节点数
        return size - set.size();
    }
}

class UnionFind {
    int[] roots;
    
    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
    }

    public int find(int i) {
        while (i != roots[i]) {
            roots[i] = roots[roots[i]];
            i = roots[i];
        }
        return i;
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
        }
    }
}

6.连通网络的操作次数(1319 - 中)

题目描述:用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。

网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。

给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。

示例 :

输入:n = 4, connections = [[0,1],[0,2],[1,2]]
输出:1
解释:拔下计算机 1 和 2 之间的线缆,并将它插到计算机 1 和 3 上。

思路:最终实现目标:将所有网络连接成同一个网络。类似547省份问题。注意两个问题:

  • 线要够,即n个节点至少需要n-1条线
  • 三个连通块至少需要两个边(即需要两条多余的线),问题转化为:最少操作数(多余线缆)= 连通块数量 - 1

代码实现:

class Solution {
    public int makeConnected(int n, int[][] connections) {
        int num = connections.length;
        if (num < n - 1) return -1;  // 线不够
        UnionFind uf = new UnionFind(n);
        
        for (int[] connection : connections) {
            uf.union(connection[0], connection[1]);
        }
        return uf.count - 1;   
    }
}

class UnionFind {
    int[] roots;
    int count;  // 记录连通块的数量
    
    public UnionFind(int n) {
        roots = new int[n];
        for (int i = 0; i < n; i++) {
            roots[i] = i;
        }
        count = n;  
    }

    public int find(int i) {
        while (i != roots[i]) {
            roots[i] = roots[roots[i]];
            i = roots[i];
        }
        return i;
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            roots[pRoot] = qRoot;
            count--;   
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容