并查集
并查集
用于合并集合和查询元素所属集合(两个元素是否属于同一集合)。
将每个集合看做成是一颗树,合并元素 \(u\) 和元素 \(v\) 的集合时,只需要将元素 \(u\) 所在的树的根结点变成元素 \(v\) 所在的树的根结点的父亲即可。
查询也是同样的,只需要查询两个元素所在的集合的根结点是否相同即可。
暴力
#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\) 表示。