算法学习:并查集

一、背景

当我们遇到朋友圈问题时:
a与b是朋友,b和c是朋友,那么我们可以将a、b、c看作一个朋友圈...此外,还有d、f、e等人,他们之间也存在着朋友关系或者没有朋友关系。
问:有多少个朋友圈?d、e是否有朋友关系?等等问题,我们都可以用并查集解决。

二、概念及其实现原理

并查集是一种用于解决组团、配对问题的数据结构,它维护的是一堆集合而不是一个集合

它需要实现的功能有:

  • makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。


    初始化
  • unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。


    合并

    但是,这样的结构是不方便使用的,我们可以压缩路径,将子节点直接指向带头节点。(可以在find的过程中处理)


    压缩路径
  • find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。

三、Java实现

class UnionFind {
    // 分组个数
    int count;
    // 数组的第i位表示i的领头结点
    int[] parent;

    // 初始化
    public UnionFind(int n) {
        count = n;
        parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // 查找x的领头结点
    public int find(int x) {
        List<Integer> list = new ArrayList<>();
        while (x != parent[x]) {
            list.add(x);
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        // 优化路径
        for (int item : list) {
            parent[item] = x;
        }
        return x;
    }

    // 合并分组
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) {
            return;
        }
        parent[rootY] = rootX;
        count--;
    }
}

五、相关算法题

547. 省份数量

问题描述

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。

示例

解题思路

实质是朋友圈问题,直接套用并查集即可。

代码示例(JAVA)

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int people = isConnected.length;
        UnionFind unionFind = new UnionFind(people);
        for (int i = 0; i < people; i++) {
            for (int j = 0; j < people; j++) {
                if (isConnected[i][j] == 1) {
                    unionFind.union(i, j);
                }
            }
        }
        return unionFind.count;
    }

    class UnionFind {
        // 分组个数
        int count;
        // 数组的第i位表示i的领头结点
        int[] parent;

        // 初始化
        public UnionFind(int n) {
            count = n;
            parent = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }

        // 查找x的领头结点
        public int find(int x) {
            List<Integer> list = new ArrayList<>();
            while (x != parent[x]) {
                list.add(x);
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            // 优化路径
            for (int item : list) {
                parent[item] = x;
            }
            return x;
        }

        // 合并分组
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            parent[rootY] = rootX;
            count--;
        }
    }
}

200. 岛屿数量

问题描述

给你一个由 1(陆地)和 0(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

示例

解题思路

这道题同样可以看作朋友圈问题,假设二维网格的长宽为mn,那么共有m * n个人;那么朋友关系如何建立?可以通过节点四周的连接情况进行判断。
另外要注意的是,并查集中的count要记录的是陆地的分组数量。

代码示例(JAVA)

class Solution {

    static int[][] xys = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int numIslands(char[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        // 建并查集
        UnionFind unionFind = new UnionFind(grid);
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j] == '1') {
                    for (int[] xy : xys) {
                        // 遍历周围的陆地/水情况,合并分组
                        if (i + xy[0] >= 0 && i + xy[0] < m && j + xy[1] >= 0 && j + xy[1] < n
                                && grid[i + xy[0]][j + xy[1]] == '1') {
                            unionFind.union(i * n + j, (i + xy[0]) * n + (j + xy[1]));
                        }
                    }
                }
            }
        }

        return unionFind.count;
    }

    class UnionFind {
        // 分组个数(陆地)
        int count;
        // 数组的第i位表示i的领头结点
        int[] parent;

        // 初始化
        public UnionFind(char[][] grid) {
            count = 0;
            int m = grid.length;
            int n = grid[0].length;
            parent = new int[m * n];
            for (int i = 0; i < m * n; i++) {
                parent[i] = i;
                if (grid[i / n][i % n] == '1') {
                    count++;
                }
            }
        }

        // 查找x的领头结点
        public int find(int x) {
            List<Integer> list = new ArrayList<>();
            while (x != parent[x]) {
                list.add(x);
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            // 优化路径
            for (int item : list) {
                parent[item] = x;
            }
            return x;
        }

        // 合并分组
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            parent[rootY] = rootX;
            count--;
        }
    }
}

130. 被围绕的区域

问题描述

给你一个 m x n 的矩阵 board ,由若干字符 XO ,找到所有被 X 围绕的区域,并将这些区域里所有的 OX 填充。

示例

解题思路

找到问题的核心:保留边界的O及与其有连通性的O,其他均为X
我们依旧可以使用并查集,我们可以建一个虚拟节点,将边界的O及与其有连通性的O与其建立分组关系;最后,遍历矩阵,将和这个虚拟节点没有分组关系的节点,设为X

代码示例(JAVA)

class Solution {
    static int[][] xys = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public void solve(char[][] board) {
        int m = board.length;
        int n = board[0].length;

        // 建立并查集,多出的节点为虚拟节点
        UnionFind unionFind = new UnionFind(m * n + 1);
        int virtualNode = m * n;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    // 周围的'O'直接和虚拟节点合并
                    if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
                        unionFind.union(i * n + j, virtualNode);
                    } else {
                        // 中间的'O'和周围的'O'合并
                        for (int[] xy : xys) {
                            if (i + xy[0] >= 0 && i + xy[0] < m
                                    && j + xy[1] >= 0 && j + xy[1] < n
                                    && board[i + xy[0]][j + xy[1]] == 'O') {
                                unionFind.union(i * n + j, (i + xy[0]) * n + j + xy[1]);
                            }
                        }
                    }
                }
            }
        }
        // 和虚拟节点没有连通性的'O'改为'X'
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (unionFind.find(i * n + j) != unionFind.find(virtualNode)) {
                    board[i][j] = 'X';
                }
            }
        }
    }

    class UnionFind {
        // 分组个数
        int count;
        // 数组的第i位表示i的领头结点
        int[] parent;

        // 初始化
        public UnionFind(int n) {
            count = n;
            parent = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }

        // 查找x的领头结点
        public int find(int x) {
            List<Integer> list = new ArrayList<>();
            while (x != parent[x]) {
                list.add(x);
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            // 优化路径
            for (int item : list) {
                parent[item] = x;
            }
            return x;
        }

        // 合并分组
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            parent[rootY] = rootX;
            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

推荐阅读更多精彩内容