【算法】并查集总结

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

一、前言

并查集(Union Find): 是一种用来管理元素分组情况的数据结构。

并查集的关键在于如何把原问题转化成图的动态连通性问题。

并查集-2022-08-0510-45-07.png

连通是一种等价关系,具有如下 3 个性质:

  1. 自反性:节点 pp 是连通的。
  2. 对称性:如果节点 pq 连通,那么 qp 也连通。
  3. 传递性:如果节点 pq 连通,qr 连通,那么 pr 也连通。

并查集算法主要实现这两个功能:

  1. union(p, q)pq 两节点连通起来: 只需将它们的根节点连通起来即可。
  2. connect(p, q)判断 pq 两节点是否连通: 判断是否有相同的根节点。

并查集-2022-08-0511-15-10.png

实现此功能的方法:记录每个节点的父节点。可以用数组表示。

并查集-2022-08-0516-32-30.png

平衡性优化:为避免生成单边树,降低树高。

  1. union(p, q)时,小树接到大树下: 需要额外数组 size[] 来记录每棵树的节点数量。
  2. 进行路径压缩: 尽量挂在根节点下。(每次查找时,进行压缩)

并查集-2022-08-0516-38-56.png

完整代码结构如下:

public class UnionFind {
    private int[] parent, size;

    public UnionFind(int n) {
        parent = new int[n];  // 存储每个节点的父节点
        size = new int[n];    // 记录每棵树的 “重量”
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 1. 查找
    public int find(int x) {
        return findPathCompressionIterative(x);
    }
    
    // 迭代查找,伴随路径压缩
    private int findPathCompressionIterative(int x) {
        int root = x;
        // 1. 查找根节点
        while (parent[root] != root) root = parent[root];
        // 2. 路径压缩:把查找路径上所有节点的父节点都更新为 根节点root
        while (parent[x] != root) {
            int tmp = parent[x];
            parent[x] = root;
            x = tmp;
        }
        return root;
    }

    // 2. 联合
    public void union(int x, int y) {
        int xRoot = find(x);
        int yRoot = find(y);
        if (xRoot == yRoot) return;

        // 小树接到大树下面,较平衡
        if (size[xRoot] < size[yRoot]) {
            parent[xRoot] = yRoot;
            size[yRoot] += size[xRoot];
        } else {
            parent[yRoot] = xRoot;
            size[xRoot] += size[yRoot];
        }
    }
}
复制代码

总结下: 算法3个关键点

  1. parent[] 记录每个节点的父节点: 相当于指向父节点的指针,所以 parent[] 内实际存储着一个森林
  2. size[] 数组记录每棵树的重量: 目的是调用 union 后树依然拥有平衡性,而不会退化成链表,影响操作效率。
  3. find 函数中进行路径压缩: 保证任意树的高度保持在常数,使得 unionconnected API 时间复杂度为 O(1)



二、题目

(2)省份数量(中)

LeetCode 547

题干分析

这个题目说的是,给你 0 到 n-1 共 n 个城市,城市之间有的相互连接,有的则不相连。如果城市 0 与城市 1 直接相连,城市 1 与城市 2 直接相连,那么城市 0 与城市 2 称为间接相连。

直接相连或间接相连的一组城市定义为一个省份。现在给你一个 n x n 的矩阵 a 表示城市之间的连接情况。a(i, j) 等于 1 表示第 i 个城市和第 j 个城市直接相连,a(i, j) 等于 0 则表示这两个城市不直接相连。你要计算出,这 n 个城市一共构成了多少个省份。

# 比如说,给你 3 个城市:
0, 1, 2

# 它们对应的连接矩阵 a 是:
1, 0, 0
0, 1, 1
0, 1, 1

# 根据这个矩阵,我们可以知道城市 0 不与城市 1 或城市 2 相连,它自成一个省份。城市 1 与城市 2 相互连接,构成一个省份。

# 因此,这 3 个城市构成了 2 个省份。
复制代码

思路解法

思路有两种: 并查集 和 暴力法(DFS)

方法一:并查集

  1. 初始化并查集
  2. 构建并查集
    1. 查看 ij 是否相连
    2. 相连就合并

AC 代码:

// 方法一: 并查集
// Time: O(n^2), Space: O(n), Faster: 86.14%
public int findCircleNum(int[][] isConnected) {
    if (isConnected == null || isConnected.length == 0 ||
        isConnected[0] == null || isConnected[0].length == 0) {
        return 0;
    }

    // 1. 初始化并查集
    int n = isConnected.length;
    UnionFind uf = new UnionFind(n);

    // 2. 构建并查集
    for (int i = 0; i < n; ++i) {
        // 2.1 查看 i 和 j 是否相连
        for (int j = i + 1; j < n; ++j) {
            // 2.2 相连就合并
            if (isConnected[i][j] == 1) uf.union(i, j);
        }
    }

    return uf.count();
}
public class UnionFind {

    private int[] parent, size;
    private int cnt; // 统计省数量

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

    public int find(int x) {
        if (parent[x] == x) return x;
        parent[x] = find(parent[x]);
        return parent[x];
    }

    // Time: O(a(n)), Space: O(1)
    public void union(int x, int y) {
        int xRoot = find(x);
        int yRoot = find(y);
        if (xRoot == yRoot) return;
        if (size[xRoot] < size[yRoot]) {
            parent[xRoot] = yRoot;
            size[yRoot] += size[xRoot];
        } else {
            parent[yRoot] = xRoot;
            size[xRoot] += size[yRoot];
        }
        --cnt; // 每合并一次,说明能省的 -1
    }

    public int count() {
        // 或者看 parent[x] == x 有几个
        return cnt;
    }
}
复制代码

方法二:暴力法 DFS,使用额外辅助

AC 代码:

// 方法二: 暴力法 DFS
// Time: O(n^2), Space: O(n), Faster: 86.14%
public int findCircleNumDFS(int[][] isConnected) {
    if (isConnected == null || isConnected.length == 0 ||
        isConnected[0] == null || isConnected[0].length == 0) {
        return 0;
    }

    int n = isConnected.length;
    boolean[] isVisited = new boolean[n];

    int cnt = 0;
    for (int i = 0; i < n; ++i) {
        if (!isVisited[i]) {
            ++ cnt;
            dfs(n, i, isConnected, isVisited);
        }
    }

    return cnt;
}

private void dfs(int n, int i, int[][] isConnected, boolean[] isVisited) {
    if (i >= n || i < 0) return;

    for (int j = 0; j < n; ++j) {
        if (j == i) continue;
        if (isConnected[i][j] == 1 && !isVisited[j]) {
            isVisited[j] = true;
            dfs(n, j, isConnected, isVisited);
        }
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/7128441346523660318