AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

并查集入门

作者: 作者的头像   张立斌 ,  2019-10-27 01:04:02 ,  所有人可见 ,  阅读 1570


5


4

并查集简介

英文名: Union & Find

并查集是一种树形结构,用于处理集合的合并和查询。

实现:

  • 每个集合用一棵树表示
  • 每个节点存储它的父节点
  • 树根的编号代表集合的编号

优化:

  • 按秩合并
  • 路径压缩: 推荐
  • 路径压缩 和 按秩合并: 矛盾
  • 路径压缩 和 按量合并: 自己提出

5个O(1):

  • findRoot(x): 平均O(1) 找到集合编号
  • isConnected(x,y): 平均O(1) 判断2个节点是否属于同一个集合
  • toUnion(x,y): 平均O(1) 合并2个节点所属的集合
  • count(x): 平均O(1) 获取节点所属集合的节点的数量
  • size(): O(1) 获取总的集合的数量

路径压缩 的时间复杂度

1、多次 findRoot 的时间复杂度平均 O(1)

findRoot前:      findRoot(7):        再次findRoot(7):
                 时复 O(log(n))      时复 O(1)
    1                 1                   1
   / \            / / | \             / / | \
  2   3          2 3  4  7           2 3  4  7
 / \  |          | |                 | |
4   5 6          5 6                 5 6
|
7

2、findRoot 的时间复杂度最差 O(log(n)),小常数的 O(log(n)) 可以看作 O(1)

路径压缩 和 按秩合并 同时使用的2个问题

1、活跃的集合 和 不活跃的集合合并,不活跃的集合的 rank 大的概率更高

操作 toUnion(1, 7)
合并前:             按秩合并后:         按量合并后:
     1       7              7                 1
 / / | \ \   |            /   \          / / / \ \ \
2 3  4  5 6  8           1     8        2 3 4   5 6 7
             |       / / | \ \  \                   |
             9      2 3  4  5 6  9                  8
                                                    |
                                                    9

虽然 按量合并 的高度大于 按秩合并,但是之后的 路径压缩 需要调整节点少。

2、使用 路径压缩 时,无法正确维持 rank

findRoot前:         findRoot(4)后:
rank: 3             rank: 3 (错误)
  1                      1
 / \                 / / | \
2   5               2 3  4  5
|   |                       |
3   6                       6
|
4

findRoot 无法正确维持 rank,即使用 路径压缩 时,无法正确使用 按秩合并。

并查集的通用模板

class UnionFind {
  struct Node {
    explicit Node(int root = 0) : root(root), cnt(1) {}
    int root;
    int cnt;
  };

 public:
  explicit UnionFind(int capacity) : nodeVec(capacity), size0(capacity) {
    for (int x = 0; x < capacity; ++x) {
      nodeVec[x].root = x;
    }
  }
  inline bool isConnected(int x, int y) { return findRoot(x) == findRoot(y); }
  bool toUnion(int x, int y) {
    int xRoot = findRoot(x);
    int yRoot = findRoot(y);
    if (xRoot != yRoot) {
      if (nodeVec[xRoot].cnt < nodeVec[yRoot].cnt) {
        nodeVec[xRoot].root = yRoot;
        nodeVec[yRoot].cnt += nodeVec[xRoot].cnt;
      } else {
        nodeVec[yRoot].root = xRoot;
        nodeVec[xRoot].cnt += nodeVec[yRoot].cnt;
      }
      --size0;
      return true;
    }
    return false;
  }
  inline int count(int x) { return nodeVec[findRoot(x)].cnt; }
  inline int size() const { return size0; }

 private:
  int findRoot(int x) {
    int root = x;
    while (nodeVec[root].root != root) {
      root = nodeVec[root].root;
    }
    while (nodeVec[x].root != root) {
      const int y = nodeVec[x].root;
      nodeVec[x].root = root;
      x = y;
    }
    return root;
  }
  vector<Node> nodeVec;
  int size0;
};

并查集的基础题目

AcWing 836. 合并集合

使用并查集的通用模板

#include <iostream>
#include <vector>
using namespace std;

int main(void) {
  int n, m, a, b;
  char oper[4];
  scanf("%d%d", &n, &m);
  UnionFind unionFind(n);
  for (int i = 0; i < m; ++i) {
    scanf("%1s%d%d", oper, &a, &b);
    if (oper[0] == 'M') {
      unionFind.toUnion(a - 1, b - 1);
    } else if (oper[0] == 'Q') {
      puts(unionFind.isConnected(a - 1, b - 1) ? "Yes" : "No");
    }
  }
  return 0;
}

AcWing 837. 连通块中点的数量

使用并查集的通用模板

#include <cstring>
#include <iostream>
#include <vector>
using namespace std;

int main(void) {
  int n, m, a, b;
  char oper[4];
  scanf("%d%d", &n, &m);
  UnionFind unionFind(n);
  for (int i = 0; i < m; ++i) {
    scanf("%2s", oper);
    if (!strcmp("C", oper)) {
      scanf("%d%d", &a, &b);
      unionFind.toUnion(a - 1, b - 1);
    } else if (!strcmp("Q1", oper)) {
      scanf("%d%d", &a, &b);
      puts(unionFind.isConnected(a - 1, b - 1) ? "Yes" : "No");
    } else if (!strcmp("Q2", oper)) {
      scanf("%d", &a);
      printf("%d\n", unionFind.count(a - 1));
    }
  }
  return 0;
}

LeetCode 200. Number of Islands

使用并查集的通用模板

class Solution {
 public:
  static int numIslands(const vector<vector<char>>& grid) {
    if (grid.empty() || grid[0].empty()) {
      return 0;
    }
    const int m = static_cast<int>(grid.size());
    const int n = static_cast<int>(grid[0].size());
    UnionFind unionFind(m * n);
    int landSize = 0;
    for (int i = 0; i < m; ++i) {
      for (int j = 0; j < n; ++j) {
        if (grid[i][j] == '1') {
          if (i > 0 && grid[i - 1][j] == '1') {
            unionFind.toUnion((i - 1) * n + j, i * n + j);
          }
          if (j > 0 && grid[i][j - 1] == '1') {
            unionFind.toUnion(i * n + j, i * n + j - 1);
          }
          ++landSize;
        }
      }
    }
    landSize -= m * n - unionFind.size();
    return landSize;
  }
};

LeetCode 547. Friend Circles

使用并查集的通用模板

class Solution {
 public:
  static int findCircleNum(const vector<vector<int>>& mat) {
    if (mat.empty() || mat.size() != mat[0].size()) {
      return 0;
    }
    const int n = static_cast<int>(mat.size());
    UnionFind unionFind(n);
    for (int i = 1; i < n; ++i) {
      for (int j = 0; j < i; ++j) {
        if (mat[i][j]) {
          unionFind.toUnion(i, j);
        }
      }
    }
    return unionFind.size();
  }
};

LeetCode 684. Redundant Connection

使用并查集的通用模板

class Solution {
 public:
  static vector<int> findRedundantConnection(const vector<vector<int>>& edges) {
    if (edges.empty()) {
      return {-1, -1};
    }
    const int n = static_cast<int>(edges.size());
    UnionFind unionFind(n + 1);
    for (int i = 0; i < n; ++i) {
      const int u = edges[i][0];
      const int v = edges[i][1];
      if (!unionFind.toUnion(u, v)) {
        return {u, v};
      }
    }
    return {-1, -1};
  }
};

AcWing 859. Kruskal算法求最小生成树

使用并查集的通用模板

#include <algorithm>
#include <climits>
#include <iostream>
#include <vector>
using namespace std;

class Graph {
  struct Edge {
    int v0;
    int v1;
    int weight;
  };

 public:
  explicit Graph(int vertexSize, int edgeCapacity = 256) : vertexSize0(vertexSize) {
    edgeVec.reserve(edgeCapacity);
  }
  void push(int v0, int v1, int weight) { edgeVec.push_back({v0, v1, weight}); }
  int sumMinTreeDist() {
    sort(edgeVec.begin(), edgeVec.end(),
         [](const Edge &edge0, const Edge &edge1) { return edge0.weight < edge1.weight; });
    UnionFind unionFind(vertexSize0);
    const int edgeSize0 = static_cast<int>(edgeVec.size());
    int res = 0;
    for (int i = 0; i < edgeSize0; ++i) {
      if (unionFind.toUnion(edgeVec[i].v0, edgeVec[i].v1)) {
        res += edgeVec[i].weight;
      }
    }
    if (unionFind.size() != 1) {
      return DIST_MAX;
    }
    return res;
  }
  static constexpr int DIST_MAX = INT_MAX / 2;

 private:
  int vertexSize0;
  vector<Edge> edgeVec;
};
constexpr int Graph::DIST_MAX;

int main(void) {
  int n, m, u, v, w;
  scanf("%d%d", &n, &m);
  Graph graph(n, m);
  for (int i = 0; i < m; ++i) {
    scanf("%d%d%d", &u, &v, &w);
    graph.push(u - 1, v - 1, w);
  }
  const int sumDist = graph.sumMinTreeDist();
  if (sumDist == Graph::DIST_MAX) {
    puts("impossible");
  } else {
    printf("%d", sumDist);
  }
  return 0;
}

8 评论


用户头像
墨染空   2019-10-28 06:22         踩      回复

按秩合并和路径压缩不冲突吧

用户头像
墨染空   2019-10-28 06:28         踩      回复

而且你单用路径压缩复杂度不是log的吗

用户头像
张立斌   2019-10-28 13:33         踩      回复

添加说明 “路径压缩 和 按秩合并 同时使用的2个问题”

用户头像
张立斌   2019-10-28 13:33    回复了 墨染空 的评论         踩      回复

添加说明 “路径压缩 的时间复杂度”

用户头像
墨染空   2019-10-28 20:07    回复了 张立斌 的评论         踩      回复

我的“秩”指的是子树大小。按秩合并有两个吧,一个用树高合并,一个用子树大小合并

用户头像
张立斌   2019-10-28 22:37    回复了 墨染空 的评论         踩      回复

路径压缩 和 按子树大小合并 一起结合使用


用户头像
御_Dragon   2019-10-27 14:07         踩      回复

并查集的重点、精髓没有讲清楚

用户头像
张立斌   2019-10-27 15:57         踩      回复

谢谢,我尽快完善下并查集的知识。您可以推荐一些资料。


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息