并查集面试题

386 阅读3分钟

「这是我参与2022首次更文挑战的第37天,活动详情查看:2022首次更文挑战」。

一、朋友圈

LeetCode

现在LeetCode上朋友圈问题改成了城市圈问题(一个省下的城市在一起),道理是一样的

1、分析

二维数组是一个正方形的matrix,N*N代表人物之间的关系,1代表认识,0代表不认识

  • m[i][i]:对角线代表自己肯定认识自己,全都是1

  • m[i][j] = 1:i人物和j人物认识,那么m[j][i] = 1,j人物和i人物认识,即互相认识

0认识2,0也认识4,所以{0,2,4}是一个朋友圈

1认识3,所以{1,3}是一个朋友圈

所以总共产生2个朋友圈,产生朋友圈的过程就是并查集union的过程

只看对角线上半部分,对角线及对角线下半部分不用看,因为对角线上自己认识自己,对角线下半部分和对角线上半部分是对称的

2、实现

public static int findCircleNum(int[][] M) {
    int N = M.length;
    // {0} {1} {2} {N-1}
    UnionFind unionFind = new UnionFind(N);
    for (int i = 0; i < N; i++) {
        for (int j = i + 1; j < N; j++) {
            if (M[i][j] == 1) { // i和j互相认识
                unionFind.union(i, j);
            }
        }
    }
    return unionFind.sets();
}

public static class UnionFind {
    // parent[i] = k : i的父亲是k
    private int[] parent;
    // size[i] = k : 如果i是代表节点,size[i]才有意义,否则无意义
    // i所在的集合大小是多少
    private int[] size;
    // 辅助结构
    private int[] help;
    // 一共有多少个集合
    private int sets;

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

    // 从i开始一直往上,往上到不能再往上,代表节点,返回
    // 这个过程要做路径压缩
    private int find(int i) {
        int hi = 0;
        while (i != parent[i]) {
            help[hi++] = i;
            i = parent[i];
        }
        for (hi--; hi >= 0; hi--) {
            parent[help[hi]] = i;
        }
        return i;
    }

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

    public int sets() {
        return sets;
    }
}

二、岛问题

给定一个二维数组matrix,里面的值不是1就是0,

上、下、左、右相邻的1认为是一片岛,

返回matrix中岛的数量

LeetCode

1、分析

方法一:遍历每个位置,发现1,则进行上下左右感染,把1变成2(标记感染过),每个位置碰5遍,时间复杂度O(N*M),整体方向从左往右,从上往下

方法二:利用并查集(HashMap),规定查左边和上边(←↑),如果右边有1,则向左就能合并到一起,如果下边有1,则向上就能合并到一起,所以只需要查询两个方向即可

怎么确定每个点不同?每个点封装一下即可

方法三:利用并查集(数组),一个N*M的matrix,定义一个长度为N*M的数组arr,把matrix中的每个位置都放到数组arr中,怎么放?公式:i*列数 + j

2、实现

2.1、递归实现

public static int numIslands(char[][] board) {
    int islands = 0;
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board[0].length; j++) {
            if (board[i][j] == '1') {
                islands++;
                infect(board, i, j);
            }
        }
    }
    return islands;
}

// 从(i,j)这个位置出发,把所有练成一片的'1'字符,变成0
private static void infect(char[][] board, int i, int j) {
    if (i < 0 || i == board.length || j < 0 || j == board[0].length || board[i][j] != '1') {
        return;
    }
    board[i][j] = 0;
    // 上
    infect(board, i - 1, j);
    // 下
    infect(board, i + 1, j);
    // 左
    infect(board, i, j - 1);
    // 右
    infect(board, i, j + 1);
}

2.2、并查集(HashMap)实现

public static int numIslands(char[][] board) {
    int row = board.length;
    int col = board[0].length;
    Dot[][] dots = new Dot[row][col];
    List<Dot> dotList = new ArrayList<>();
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            if (board[i][j] == '1') {
                dots[i][j] = new Dot();
                dotList.add(dots[i][j]);
            }
        }
    }
    UnionFind<Dot> uf = new UnionFind<>(dotList);
    // 0行填完
    for (int j = 1; j < col; j++) {
        // (0,j)  (0,0)跳过了  (0,1) (0,2) (0,3)
        if (board[0][j - 1] == '1' && board[0][j] == '1') {
            uf.union(dots[0][j - 1], dots[0][j]);
        }
    }
    // 0列填完
    for (int i = 1; i < row; i++) {
        if (board[i - 1][0] == '1' && board[i][0] == '1') {
            uf.union(dots[i - 1][0], dots[i][0]);
        }
    }
    // 中间填完
    for (int i = 1; i < row; i++) {
        for (int j = 1; j < col; j++) {
            if (board[i][j] == '1') {
                if (board[i][j - 1] == '1') {
                    uf.union(dots[i][j - 1], dots[i][j]);
                }
                if (board[i - 1][j] == '1') {
                    uf.union(dots[i - 1][j], dots[i][j]);
                }
            }
        }
    }
    return uf.sets();
}

public static class Dot {

}

public static class Node<V> {

    V value;

    public Node(V v) {
        value = v;
    }

}

public static class UnionFind<V> {
    public HashMap<V, Node<V>> nodes;
    public HashMap<Node<V>, Node<V>> parents;
    public HashMap<Node<V>, Integer> sizeMap;

    public UnionFind(List<V> values) {
        nodes = new HashMap<>();
        parents = new HashMap<>();
        sizeMap = new HashMap<>();
        for (V cur : values) {
            Node<V> node = new Node<>(cur);
            nodes.put(cur, node);
            parents.put(node, node);
            sizeMap.put(node, 1);
        }
    }

    public Node<V> findFather(Node<V> cur) {
        Stack<Node<V>> path = new Stack<>();
        while (cur != parents.get(cur)) {
            path.push(cur);
            cur = parents.get(cur);
        }
        while (!path.isEmpty()) {
            parents.put(path.pop(), cur);
        }
        return cur;
    }

    public void union(V a, V b) {
        Node<V> aHead = findFather(nodes.get(a));
        Node<V> bHead = findFather(nodes.get(b));
        if (aHead != bHead) {
            int aSetSize = sizeMap.get(aHead);
            int bSetSize = sizeMap.get(bHead);
            Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
            Node<V> small = big == aHead ? bHead : aHead;
            parents.put(small, big);
            sizeMap.put(big, aSetSize + bSetSize);
            sizeMap.remove(small);
        }
    }

    public int sets() {
        return sizeMap.size();
    }

}

2.3、并查集(数组)实现

public static int numIslands(char[][] board) {
    int row = board.length;
    int col = board[0].length;
    UnionFind uf = new UnionFind(board);
    for (int j = 1; j < col; j++) {
        if (board[0][j - 1] == '1' && board[0][j] == '1') {
            uf.union(0, j - 1, 0, j);
        }
    }
    for (int i = 1; i < row; i++) {
        if (board[i - 1][0] == '1' && board[i][0] == '1') {
            uf.union(i - 1, 0, i, 0);
        }
    }
    for (int i = 1; i < row; i++) {
        for (int j = 1; j < col; j++) {
            if (board[i][j] == '1') {
                if (board[i][j - 1] == '1') {
                    uf.union(i, j - 1, i, j);
                }
                if (board[i - 1][j] == '1') {
                    uf.union(i - 1, j, i, j);
                }
            }
        }
    }
    return uf.sets();
}

public static class UnionFind {
    private int[] parent;
    private int[] size;
    private int[] help;
    private int col;
    private int sets;

    public UnionFind(char[][] board) {
        col = board[0].length;
        sets = 0;
        int row = board.length;
        int len = row * col;
        parent = new int[len];
        size = new int[len];
        help = new int[len];
        for (int r = 0; r < row; r++) {
            for (int c = 0; c < col; c++) {
                if (board[r][c] == '1') {
                    int i = index(r, c);
                    parent[i] = i;
                    size[i] = 1;
                    sets++;
                }
            }
        }
    }

    // (r,c) -> i
    private int index(int r, int c) {
        return r * col + c;
    }

    // 原始位置 -> 下标
    private int find(int i) {
        int hi = 0;
        while (i != parent[i]) {
            help[hi++] = i;
            i = parent[i];
        }
        for (hi--; hi >= 0; hi--) {
            parent[help[hi]] = i;
        }
        return i;
    }

    public void union(int r1, int c1, int r2, int c2) {
        int i1 = index(r1, c1);
        int i2 = index(r2, c2);
        int f1 = find(i1);
        int f2 = find(i2);
        if (f1 != f2) {
            if (size[f1] >= size[f2]) {
                size[f1] += size[f2];
                parent[f2] = f1;
            } else {
                size[f2] += size[f1];
                parent[f1] = f2;
            }
            sets--;
        }
    }

    public int sets() {
        return sets;
    }

}

三、总结

并查集不仅仅可以通过容器实现,也可以通过数组实现。

并查集介绍