使用並查集解決的相關問題

Grey Zeng發表於2022-06-04

作者: Grey

原文地址:使用並查集解決的相關問題

關於並查集的說明,見如下部落格:

使用並查集處理集合的合併和查詢問題

相關題目

LeetCode 200. 島嶼數量

本題的解題思路參考部落格

使用DFS和並查集方法解決島問題

LeetCode 547. 省份數量

主要思路

橫縱座標表示的是城市,因為城市是一樣的,所以只需要遍歷對角線上半區或者下半區即可,如果某個(i,j)位置是1,可以說明如下兩個情況

第一,i這座城市和j這座城市可以做union操作。

第二,(j,i)位置一定也是1。

遍歷完畢後,返回整個並查集中的集合數量即可。

完整程式碼

public static int findCircleNum(int[][] m) {
        int n = m.length;
        UF uf = new UF(n);
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (m[i][j] == 1) {
                    uf.union(i, j);
                }
            }
        }
        return uf.setSize();
    }

    public static class UF {
        int[] parent;
        int[] help;
        int[] size;
        int sets;

        public UF(int n) {
            size = new int[n];
            parent = new int[n];
            help = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                size[i] = 1;
            }
            sets = n;
        }

        public void union(int i, int j) {
            if (i == j) {
                return;
            }
            int p1 = find(i);
            int p2 = find(j);
            if (p2 != p1) {
                int size1 = size[p1];
                int size2 = size[p2];
                if (size1 > size2) {
                    parent[p2] = p1;
                    size[p1] += size2;
                } else {
                    parent[p1] = p2;
                    size[p2] += size1;
                }
                sets--;
            }
        }

        public int find(int i) {
            int hi = 0;
            while (i != parent[i]) {
                help[hi++] = i;
                i = parent[i];
            }
            for (int index = 0; index < hi; index++) {
                parent[help[index]] = i;
            }
            return i;
        }

        public int setSize() {
            return sets;
        }
    }

LeetCode 305. 島嶼數量II

本題和LeetCode 200. 島嶼數量最大的區別就是,本題是依次給具體的島嶼,並查集要實時union合適的兩個島嶼,思路也一樣,初始化整個地圖上都是水單元格,且整個地圖的島嶼數量初始為0,如果某個位置來一個島嶼,先將此位置的代表節點設定為本身,將此位置的集合大小設定為1,然後和其上下左右方向有島嶼的點union一下,並將集合數量減一,返回集合數量即可,完整程式碼見:

    public static List<Integer> numIslands2(int m, int n, int[][] positions) {
        UF uf = new UF(m, n);
        List<Integer> ans = new ArrayList<>();
        for (int[] position : positions) {
            ans.add(uf.connect(position[0], position[1]));
        }
        return ans;
    }

    public static class UF {
        int[] help;
        int[] parent;
        int[] size;
        int sets;
        int row;
        int col;

        public UF(int m, int n) {
            row = m;
            col = n;
            int len = m * n;
            help = new int[len];
            size = new int[len];
            parent = new int[len];
        }


        private int index(int i, int j) {
            return i * col + j;
        }

        private void union(int i1, int j1, int i2, int j2) {
            if (i1 < 0 || i2 < 0 || i1 >= row || i2 >= row || j1 < 0 || j2 < 0 || j1 >= col || j2 >= col) {
                return;
            }

            int f1 = index(i1, j1);
            int f2 = index(i2, j2);
            if (size[f1] == 0 || size[f2] == 0) {
                // 重要:如果兩個都不是島嶼,則不用合併
                return;
            }
            f1 = find(f1);
            f2 = find(f2);
            if (f1 != f2) {
                int s1 = size[f1];
                int s2 = size[f2];
                if (s1 >= s2) {
                    size[f1] += s2;
                    parent[f2] = f1;
                } else {
                    size[f2] += s1;
                    parent[f1] = f2;
                }
                sets--;
            }
        }

        public int find(int i) {
            int hi = 0;
            while (i != parent[i]) {
                help[hi++] = i;
                i = parent[i];
            }
            for (int index = 0; index < hi; index++) {
                parent[help[index]] = i;
            }
            return i;
        }

        public int connect(int i, int j) {
            int index = index(i, j);
            if (size[index] == 0) {
                sets++;
                size[index] = 1;
                parent[index] = index;
                // 去四個方向union
                union(i - 1, j, i, j);
                union(i, j - 1, i, j);
                union(i + 1, j, i, j);
                union(i, j + 1, i, j);
            }
            // index上本來就有島嶼,所以不需要處理
            return sets;
        }
    }

類似問題:LintCode 434. 島嶼的個數II

LeetCode 130. 被圍繞的區域

LeetCode 200. 島嶼數量問題一樣,這個題目也有DFS和並查集兩種解決方法。DFS方法的思路是,先遍歷最外圈(即:第一行,第一列,最後一行,最後一列),最外圈中的元素如果為O,則做如下事情:

將這個元素和其相連的元素都設定為#號(或者其他的只要不是原矩陣有的字元),我們可以簡單理解為,拿最外圈的O元素去"解救"和其相連的所有元素,並打上一個標記。

然後再次遍歷整個矩陣,只要沒打上標記的(理解為沒被解救的),都設定為X,其餘的都是O

DFS解法的完整程式碼見:

    public static void solve(char[][] board) {
        if (board == null || board.length == 0 || board[0] == null || board[0].length == 0) {
            return;
        }
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            if (board[i][0] == 'O') {
                free(i, 0, board);
            }
            if (board[i][n - 1] == 'O') {
                free(i, n - 1, board);
            }
        }
        for (int i = 0; i < n; i++) {
            if (board[0][i] == 'O') {
                free(0, i, board);
            }
            if (board[m - 1][i] == 'O') {
                free(m - 1, i, board);
            }
        }
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                board[i][j] = (board[i][j] != '#' ? 'X' : 'O');
            }
        }
    }
    
    public static void free(int i, int j, char[][] board) {
        int m = board.length;
        int n = board[0].length;
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != 'O') {
            return;
        }
        board[i][j] = '#';
        free(i + 1, j, board);
        free(i - 1, j, board);
        free(i, j + 1, board);
        free(i, j - 1, board);
    }

本題還有並查集的做法,即,將矩陣第一列,最後一列,第一行,最後一行中的所有O節點的代表節點都設定為dump節點,然後遍歷矩陣的其他位置,只要是O字元的,就和其四個方向的節點進行union操作,最後,再次遍歷整個矩陣,如果是O且和dump不是同一集合的,就算是沒有被解救的點,否則,都設定為X

完整程式碼如下:

public static void solve(char[][] board) {
        if (board == null || board.length <= 2 || board[0].length <= 2) {
            return;
        }
        int m = board.length;
        int n = board[0].length;
        // 所有周邊為O的點的代表節點都設定為dump
        int dump = 0;
        UF uf = new UF(m, n);
    // 第一列和最後一列O字元的元素和dump點union一下
        for (int i = 0; i < m; i++) {
            if (board[i][0] == 'O') {
                uf.union(dump, i, 0);
            }
            if (board[i][n - 1] == 'O') {
                uf.union(dump, i, n - 1);
            }
        }
    // 第一行和最後一行O字元的元素和dump點union一下
        for (int i = 0; i < n; i++) {
            if (board[0][i] == 'O') {
                uf.union(dump, 0, i);
            }
            if (board[m - 1][i] == 'O') {
                uf.union(dump, m - 1, i);
            }
        }
    // 其他位置,只要是O字元,就和上下左右的O字元union一下
        for (int i = 1; i < m - 1; i++) {
            for (int j = 1; j < n - 1; j++) {
                if (board[i][j] == 'O') {
                    int index = uf.index(i, j);
                    if (board[i - 1][j] == 'O') {
                        uf.union(index, i - 1, j);
                    }
                    if (board[i][j - 1] == 'O') {
                        uf.union(index, i, j - 1);
                    }
                    if (board[i + 1][j] == 'O') {
                        uf.union(index, i + 1, j);
                    }
                    if (board[i][j + 1] == 'O') {
                        uf.union(index, i, j + 1);
                    }
                }
            }
        }
    // 最後判斷哪些不是和dump點在同一集合的O點,這些都會被X吞沒
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O' && !uf.isSameSet(dump, i, j)) {
                    board[i][j] = 'X';
                }
            }
        }
    }

    public static class UF {
        int[] help;
        int[] size;
        int[] parent;
        int row;
        int col;

        public UF(int m, int n) {
            row = m;
            col = n;
            // 多一個位置,用於存dump
            int len = m * n + 1;
            help = new int[len];
            size = new int[len];
            parent = new int[len];
            for (int i = 1; i < len; i++) {
                parent[i] = i;
                size[i] = 1;
            }
        }

        public int index(int i, int j) {
            return i * col + j + 1;
        }

        private int find(int i) {
            int hi = 0;
            while (i != parent[i]) {
                help[hi++] = i;
                i = parent[i];
            }
            for (int index = 0; index < hi; index++) {
                parent[help[index]] = i;
            }
            return i;
        }

        public void union(int p1, int i, int j) {
            int p2 = index(i, j);
            int f1 = find(p1);
            int f2 = find(p2);
            if (f1 != f2) {
                int s1 = size[f1];
                int s2 = size[f2];
                if (s1 >= s2) {
                    size[f1] += s2;
                    parent[f2] = f1;
                } else {
                    size[f2] += s1;
                    parent[f1] = f2;
                }
            }
        }

        public boolean isSameSet(int p, int i, int j) {
            return find(p) == find(index(i, j));
        }
    }

類似問題:LintCode 477. 被圍繞的區域

LintCode 178. 圖是否是樹

本題比較簡單,判斷條件如下:

  1. 如果n等於0,預設就是空樹,直接返回true

  2. 如果n - 1 != 邊數量,說明不是樹,因為對於有n個點的樹,邊的數量一定是n-1

  3. 最重要的一個判斷條件:如果一個邊中的的兩個點的代表節點一樣,說明出現了環,所以,最後union掉所有邊的所有節點,如果集合個數不等於1,說明有環,直接返回false

完整程式碼見:

    public static boolean validTree(int n, int[][] edges) {
        if (n == 0) {
            return true;
        }
        if (n - 1 != edges.length) {
            return false;
        }
        LeetCode_0261_GraphValidTree.UnionFind uf = new LeetCode_0261_GraphValidTree.UnionFind(n);
        for (int[] edge : edges) {
            uf.union(edge[0], edge[1]);
        }
        return uf.setSize() == 1;
    }

    // 如何判斷環? 如果一個node節點中兩個點的代表點一樣,說明出現了環,直接返回false
    public static class UnionFind {
        private int[] parents;
        private int[] size;
        private int[] help;
        private int sets;

        public UnionFind(int n) {
            parents = new int[n];
            size = new int[n];
            help = new int[n];
            for (int i = 0; i < n; i++) {
                parents[i] = i;
                size[i] = 1;
            }
            sets = n;
        }

        public int find(int i) {
            int hi = 0;
            while (i != parents[i]) {
                help[hi++] = i;
                i = parents[i];
            }
            for (int j = 0; j < hi; j++) {
                parents[help[j]] = i;
            }
            return i;

        }

        public void union(int i, int j) {
            int f1 = find(i);
            int f2 = find(j);
            if (f1 != f2) {
                int s1 = size[f1];
                int s2 = size[f2];
                if (s1 < s2) {
                    parents[f1] = parents[f2];
                    size[f2] += s1;
                } else {
                    parents[f2] = parents[f1];
                    size[f1] += s2;
                }
                sets--;
            }
        }

        public int setSize() {
            return sets;
        }
    }

類似問題:LeetCode 261. 以圖判斷樹

LeetCode 952. 按公因數計算最大元件大小

本題關鍵解法也是並查集,只不過在union過程中,需要將當前位置的所有因數得到並先儲存起來,我們可以用雜湊表來儲存這樣的關係,以6個數為例,存在雜湊表中有兩條記錄,即:

記錄1: key:3,value:6

記錄2: key: 2,value:6

當我來到下一個數的時候,如果這個數的因數有3,則我可以通過雜湊表直接找到曾經有一個6和你有共同的因數。然後就可以把這個數和6進行union操作,最後只要返回並查集中集合元素最多的那個集合數量即可。完整程式碼如下:

    public static int largestComponentSize(int[] arr) {
        UnionFind uf = new UnionFind(arr.length);
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < arr.length; i++) {
            // 以下是找arr[i]有哪些因數的相對比較快的做法。
            int num = (int) Math.sqrt(arr[i]);
            for (int j = 1; j <= num; j++) {
                if (arr[i] % j == 0) {
                    if (j != 1) {
                        if (!map.containsKey(j)) {
                            map.put(j, i);
                        } else {
                            // 找到有共同因數的元素了,可以合併了
                            uf.union(map.get(j), i);
                        }
                    }
                    int other = arr[i] / j;
                    if (other != 1) {
                        if (!map.containsKey(other)) {
                            map.put(other, i);
                        } else {
                            // 找到有共同因數的元素了,可以合併了
                            uf.union(map.get(other), i);
                        }
                    }
                }
            }
        }
        return uf.maxSize();
    }

    // 並查集
    public static class UnionFind {
        private int[] parents;
        private int[] size;
        private int[] help;

        public UnionFind(int len) {
            parents = new int[len];
            size = new int[len];
            help = new int[len];
            for (int i = 0; i < len; i++) {
                parents[i] = i;
                size[i] = 1;
            }
        }

        public int maxSize() {
            int ans = 0;
            for (int size : size) {
                ans = Math.max(ans, size);
            }
            return ans;
        }

        private int find(int i) {
            int hi = 0;
            while (i != parents[i]) {
                help[hi++] = i;
                i = parents[i];
            }
            for (int j = 0; j < hi; j++) {
                parents[help[j]] = i;
            }
            return i;
        }

        // i 和 j分別是兩個數的位置,不是值
        public void union(int i, int j) {
            int f1 = find(i);
            int f2 = find(j);
            if (f1 != f2) {
                int s1 = size[f1];
                int s2 = size[f2];
                if (s1 > s2) {
                    parents[f2] = f1;
                    size[f1] += s2;
                } else {
                    parents[f1] = f2;
                    size[f2] += s1;
                }
            }
        }
    }

更多

演算法和資料結構筆記

相關文章