Loading

并查集

并查集

用于合并集合和查询元素所属集合(两个元素是否属于同一集合)。

将每个集合看做成是一颗树,合并元素 \(u\) 和元素 \(v\) 的集合时,只需要将元素 \(u\) 所在的树的根结点变成元素 \(v\) 所在的树的根结点的父亲即可。

查询也是同样的,只需要查询两个元素所在的集合的根结点是否相同即可。

暴力

洛谷 P3367

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m, fa[N];

int Find(int t) {   // 查询根结点
  if (fa[t]) {
    return Find(fa[t]);
  }
  return t;
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  while (m--) {
    int op, u, v;
    cin >> op >> u >> v;
    if (op == 1) {
      u = Find(u), v = Find(v);
      if (u != v) {   
      // 如果将自己连为自己的父亲的话,Find() 函数会无限递归
        fa[u] = v;
      }
    } else {
      cout << (Find(u) == Find(v) ? "Y\n" : "N\n");
    }
  }
  return 0;
}

但是,在最坏的情况下,所有元素都在一个集合中,但是这颗树是一条链的话,会使得每次查询的时间达到 \(O(n)\),可能会超时,所以需要进行一些优化。

路径压缩

每次在查询根结点时,将这条路径上的所有元素的父亲结点都设成根节点。

#include <bits/stdc++.h>

using namespace std;

const int N = 1e4 + 10;

int n, m, op, x, y, fa[N];

int Find(int t) {
  return fa[t] ? fa[t] = Find(fa[t]) : t;   
  // 将父亲结点设为根结点
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> m;
  while (m--) {
    cin >> op >> x >> y;
    if (op == 1) {
      x = Find(x), y = Find(y);
      if (x != y) {
        fa[x] = y;
      }
    } else {
      cout << (Find(x) == Find(y) ? 'Y' : 'N') << '\n';
    }
  }
  return 0;
}

按秩合并(启发式合并)

按大小合并

证明:最开始,元素 \(i\) 在一个大小为 1 的集合中,考虑最坏情况,它会合并到一个大小为 2 的集合中,然后合并到一个大小为 4 的集合中……一个元素最多合并 \(\log n\) 次,所以 \(n\) 个元素合并的总时间复杂度为 \(O(n \times \log n)\)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m, fa[N], sz[N];

int Find(int t) {
  return fa[t] ? Find(fa[t]) : t;
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  fill(sz + 1, sz + n + 1, 1);
  while (m--) {
    int op, u, v;
    cin >> op >> u >> v;
    if (op == 1) {
      u = Find(u), v = Find(v);
      if (u != v) {
        if (sz[u] > sz[v]) {
          swap(u, v);
        }
        sz[v] += sz[u], fa[u] = v;
      }
    } else {
      cout << (Find(u) == Find(v) ? "Y\n" : "N\n");
    }
  }
  return 0;
}

按高度合并

时间复杂度也是 \(O(n \times \log n)\)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m, fa[N], h[N];

int Find(int t) {
  return fa[t] ? Find(fa[t]) : t;
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  while (m--) {
    int op, u, v;
    cin >> op >> u >> v;
    if (op == 1) {
      u = Find(u), v = Find(v);
      if (u != v) {
        if (h[u] > h[v]) {
          swap(u, v);
        }
        if (h[u] == h[v]) {
          h[u]++;
        }
        fa[u] = v;
      }
    } else {
      cout << (Find(u) == Find(v) ? "Y\n" : "N\n");
    }
  }
  return 0;
}

路径压缩 + 按秩合并

每次查询的时间复杂度为 \(O(\alpha (n))\),其中 \(\alpha (n)\) 指的是阿克曼函数的反函数,增长极慢,是一个很小的常数。

按大小合并

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m, fa[N], sz[N];

int Find(int t) {
  return (fa[t] ? fa[t] = Find(fa[t]) : t);
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  fill(sz + 1, sz + n + 1, 1);
  while (m--) {
    int op, u, v;
    cin >> op >> u >> v;
    if (op == 1) {
      u = Find(u), v = Find(v);
      if (u != v) {
        if (sz[u] < sz[v]) {
          swap(u, v);
        }
        sz[v] += sz[u], fa[u] = v;
      }
    } else {
      cout << (Find(u) == Find(v) ? "Y\n" : "N\n");
    }
  }
  return 0;
}

按高度合并

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m, fa[N], h[N];

int Find(int t) {
  return (fa[t] ? fa[t] = Find(fa[t]) : t);
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  while (m--) {
    int op, u, v;
    cin >> op >> u >> v;
    if (op == 1) {
      u = Find(u), v = Find(v);
      if (u != v) {
        if (h[u] > h[v]) {
          swap(u, v);
        }
        if (h[u] == h[v]) {
          h[u]++;
        }
        fa[u] = v;
      }
    } else {
      cout << (Find(u) == Find(v) ? "Y\n" : "N\n");
    }
  }
  return 0;
}

带权并查集

我们可以在每个并查集的边上设定某种权值,在路径压缩时需要将其合并,这样可以解决更多的问题。

洛谷 P2024

我们可以将同类关系的边权设为 0,捕食关系的边权设为 1,被捕食关系的边权设为 2,用一个数组来记录点 \(i\) 和点 \(i\) 的父亲的关系。

#include <bits/stdc++.h>

using namespace std;

const int N = 5e4 + 10;

int n, k, op, x, y, d[N], fa[N], ans;

int Find(int t) {
  if (!fa[t]) {
    return t;
  }
  int x = Find(fa[t]);
  d[t] = (d[t] + d[fa[t]]) % 3, fa[t] = x;
  return x;
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> k;
  while (k--) {
    cin >> op >> x >> y;
    if ((x > n || y > n) || (op == 2 && x == y)) {
      ans++;
    } else {
      int u = Find(x), v = Find(y);
      op--;
      if (u != v) {  // 不属于同一个集合
        int t = (op + d[y] - d[x] + 3) % 3;
        fa[u] = v, d[u] = t;  // d[u] 代表 u 和 fa[u] 的关系
      } else {
        int t = (d[x] - d[y] + 3) % 3; 
        // t 是集合内 x 和 y 的关系
        ans += (t != op);
      }
    }
  }
  cout << ans;
  return 0;
}

种类并查集

如果有 \(i\) 种不同的关系,就用 \(i\) 个并查集来维护一个元素和其他元素的关系。

洛谷 P2024

将吃自己的,被自己吃的,和自己同类的这三种关系分别存在三个并查集中,用结点 \(i, 2 \times i, 3 \times i\) 表示。

posted @ 2025-05-04 10:19  Yan719  阅读(18)  评论(0)    收藏  举报