前言
并查集的基本原理参见关于并查集
参考知乎专栏-算法笔记:并查集
基本代码
1 2 3 4 5 6 7 8 9 10
| int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }
p[find(a)] = find(b);
|
简单提一下,按秩合并。这里的秩rank(),具体指的是并查集合的深度。在合并时,将秩较小的集合,合并到秩较大的集合上,效率较高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void merge(int a, int b) { int a = find(a), b = find(b); if (cnt[a] <= cnt[b]) { p[a] = b; } else { p[b] = a; } if (cnt[a] == cnt[b] && a != b) { cnt[b] ++; } }
|
java类实现并查集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class DSU { int[] p, rank; public DSU(int n) { this.p = new int[n]; this.rank = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; rank[i] = 1; } } int find(int u) { if (p[u] != u) p[u] = find(p[u]); return p[u]; } void union(int x, int y) { int xx = find(x), yy = find(y); if (rank[xx] <= rank[yy]) p[xx] = yy; else p[yy] = xx; if (rank[xx] == rank[yy] && xx != yy) rank[yy]++; } }
|
以下举例的图片来自算法笔记:并查集
考虑以上极端例子,左边的集合秩为4,右边集合的秩只有1
毫无疑问,后者的效率较高,避免的大量节点的更新。
例题
连通块中点的数量
问题链接👉🏻acwing 837. 连通块中点的数量
问题描述
给定一个包含n个点(编号为 1∼n−1)的无向图,初始时图中没有边。
现在要进行m个操作,操作共有三种:
C a b
,在点a和点b之间连一条边,a和b可能相等;Q1 a b
,询问点a和点b是否在同一个连通块中,a和b可能相等;Q2 a
,询问点a所在连通块中点的数量
;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为 C a b
,Q1 a b
或 Q2 a
中的一种。
输出格式
对于每个询问指令 Q1 a b
,如果a和b在同一个连通块中,则输出 Yes
,否则输出 No
。
对于每个询问指令 Q2 a
,输出一个整数表示点a所在连通块中点的数量
每个结果占一行。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <iostream>
using namespace std; const int N = 1e5+10;
int n, m; int p[N], cnt[N];
int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }
int main() { cin >> n >> m; for (int i = 1; i <= n; ++ i) { p[i] = i; cnt[i] = 1; } while (m --) { string op; int a, b; cin >> op; if (op == "C") { cin >> a >> b; if (find(a) != find(b)) { cnt[find(b)] += cnt[find(a)]; p[find(a)] = find(b); } } else if (op == "Q1") { cin >> a >> b; if (find(a) != find(b)) puts("No"); else puts("Yes"); } else { cin >> a; cout << cnt[find(a)] << endl; } } return 0; }
|
加入计数功能,唯一需要注意的点是:在合并集和时,计数操作一定要在合并之前。
1 2 3 4 5 6 7 8
| if (find(a) != find(b)) { cnt[find(b)] += cnt[find(a)]; p[find(a)] = find(b);
}
|
否则就相当于,让b所在集合元素数量翻倍,显然不合理;
问题描述
解题思路
例子
1 2 3 4 5 6 7 8
| 100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5
|
不管是什么样的信息组合,两句话确定一个关系。
并查集中维护的是节点之间的关系,如下:
对于每个节点,维持一个到根节点的距离,模三,余1表示可以吃根节点;余2表示可以被根节点吃;余0表示和根节点是同类。
路径压缩的时候,更新为每个点到根节点的距离。
代码
1 2 3 4 5 6 7 8 9 10
|
int find(int x) { if (p[x] != x) { int t = find(p[x]); d[x] += d[p[x]]; p[x] = t; } return p[x]; }
|
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| #include <iostream>
using namespace std;
const int N = 50010;
int n, m; int p[N], d[N];
int find(int x) { if (p[x] != x) { int t = find(p[x]); d[x] += d[p[x]]; p[x] = t; } return p[x]; }
int main() { scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
int res = 0; while (m -- ) { int t, x, y; scanf("%d%d%d", &t, &x, &y);
if (x > n || y > n) res ++ ; else { int px = find(x), py = find(y); if (t == 1) { if (px == py && (d[x] - d[y]) % 3) res ++ ; else if (px != py) { p[px] = py; d[px] = d[y] - d[x]; } } else { if (px == py && (d[x] - d[y] - 1) % 3) res ++ ; else if (px != py) { p[px] = py; d[px] = d[y] + 1 - d[x]; } } } }
printf("%d\n", res);
return 0; }
|
📔博文图谱
提及本博文的链接