来源:算法讲解056【必备】并查集-上 · 2026-10-21
依据:视频字幕 2000 字
并查集(Union-Find / Disjoint Set Union, DSU)维护一堆不相交集合,支持「查某元素属于哪个集合(find)」「判断两元素是否同集合(isSameSet)」「合并两集合(union)」三种操作,均摊复杂度接近 O(1)。
parent[x],根的父亲指向自己。parent[i] = i。parent[i] = i,size[i] = 1(按大小合并用)。find(x):若 parent[x] != x,递归/迭代找根;找到后把路径上节点都指向根(路径压缩)。isSameSet(a, b):find(a) == find(b)。union(a, b): - 分别找根 ra = find(a), rb = find(b);
- 若 ra == rb 直接返回(已同集合);
- 比较 size[ra] 与 size[rb],小根挂到大根下:parent[小] = 大,size[大] += size[小]。
n = 5,元素 1..5,初始 parent = [1,2,3,4,5],size = [1,1,1,1,1],连通块数 = 5。
| 操作 | 说明 | parent 变化 | size 变化 | 连通块 |
|---|---|---|---|---|
| union(1,2) | 根 1、2,size 相等,2 挂 1 下 | parent[2]=1 | size[1]=2 | 4 |
| union(3,4) | 根 3、4,4 挂 3 下 | parent[4]=3 | size[3]=2 | 3 |
| union(2,3) | find(2)=1,find(3)=3,size[1]=2, size[3]=2,3 挂 1 下 | parent[3]=1 | size[1]=4 | 2 |
| isSameSet(4,5) | find(4)=1(路径 4→3→1 压缩为 4→1),find(5)=5,不等 | parent[4]=1 | — | 2 |
| union(4,5) | find(4)=1,find(5)=5,size[1]=4 > size[5]=1,5 挂 1 下 | parent[5]=1 | size[1]=5 | 1 |
最终所有元素根都是 1,连通块数 = 1。
class DSU:
def __init__(self, n):
# 下标 0..n-1
self.parent = list(range(n))
self.size = [1] * n
self.components = n # 连通块数量
def find(self, x):
# 迭代路径压缩,避免递归爆栈
root = x
while self.parent[root] != root:
root = self.parent[root]
# 第二遍:把路径上所有点直接挂到根
while self.parent[x] != root:
nxt = self.parent[x]
self.parent[x] = root
x = nxt
return root
def is_same_set(self, a, b):
return self.find(a) == self.find(b)
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False
# 按大小合并:小树挂大树
if self.size[ra] < self.size[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
self.size[ra] += self.size[rb]
self.components -= 1
return True
#include <bits/stdc++.h>
using namespace std;
struct DSU {
vector<int> parent, sz;
int components; // 连通块数量
DSU() {}
explicit DSU(int n) { init(n); }
// 下标 0..n-1
void init(int n) {
parent.resize(n);
sz.assign(n, 1);
components = n;
for (int i = 0; i < n; ++i) parent[i] = i;
}
// 迭代路径压缩(防爆栈,常数也小)
int find(int x) {
int root = x;
while (parent[root] != root) root = parent[root];
while (parent[x] != root) {
int nxt = parent[x];
parent[x] = root;
x = nxt;
}
return root;
}
bool isSameSet(int a, int b) { return find(a) == find(b); }
// 返回是否真的合并了(原来不同集合)
bool unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) swap(ra, rb); // 小挂大
parent[rb] = ra;
sz[ra] += sz[rb];
--components;
return true;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
DSU dsu(n);
while (m--) {
int op, a, b;
cin >> op >> a >> b;
if (op == 1) dsu.unite(a, b);
else cout << (dsu.isSameSet(a, b) ? "YES\n" : "NO\n");
}
return 0;
}
- 单次 find / union / isSameSet:均摊 O(α(n)),α(n) ≤ 4(n ≤ 1e6),实际就是常数。
- 最坏单次(不摊还)O(log n),但摊还后极低。
- m 次操作总时间 O(m·α(n)),1e6 次操作轻松过。
// 并查集模板:路径压缩 + 按大小合并
// 下标 0..n-1,若题目 1..n 请自行 -1 或把 init 改成 1..n
struct DSU {
vector<int> parent, sz;
int components;
void init(int n) {
parent.resize(n);
sz.assign(n, 1);
components = n;
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
int root = x;
while (parent[root] != root) root = parent[root];
while (parent[x] != root) {
int nxt = parent[x];
parent[x] = root;
x = nxt;
}
return root;
}
bool unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) swap(ra, rb);
parent[rb] = ra;
sz[ra] += sz[rb];
--components;
return true;
}
bool isSameSet(int a, int b) { return find(a) == find(b); }
};
多测时每次 dsu.init(n) 即可(resize + assign 会重置,不会残留上一组数据)。
--a; --b; 或 init(n+1) 用 1..n。混用必 WA。parent 和 sz 必须每组测试重新 init,否则残留上一组数据。用 init(n) 最稳。while(parent[x]!=x) x=parent[x]; return x; 不压缩,最坏 O(n) 单次,大数据 TLE。components 只在成功合并时减:ra == rb 时不能减,否则连通块数错。isSameSet 写成比较 parent 而非根:必须 find(a)==find(b),不能 parent[a]==parent[b]。id = r * m + c,注意行列哪个是 m,别搞反。unite(u,v),成功则加入 MST 并累加权值,直到选了 n-1 条边。改法:在模板外加排序和累加。components,或每次成功 union 后输出。components == 1。unite 返回 false 的边就是答案。components++,再和上下左右已存在的陆地 union(每次成功 union 再 components--)。map<int,int> 或 unordered_map 存 parent:常数巨大,1e5 以上必 TLE。必须用 vector/数组。list 或 set 维护集合:合并要搬家,O(n) 每次,完全错误方向。init,第二组开始全错。components 初始值写成 0 或 n-1。init(n) 导致下标 n 越界(应 init(n+1))。题 1:n=6,依次执行 union(1,2), union(3,4), union(2,3), union(5,6), union(4,5)(下标 1..6)。问最终连通块数和元素 1 所在集合的大小。
初始 6 块。
最终连通块数 = 1,元素 1 所在集合大小 = 6。
</details>
题 2:给 n 个点 m 条无向边(可能有重边自环),问至少加几条边使整张图连通?
对所有边做 union,最后 components 就是连通块数。答案 = components - 1(若 n=0 特判为 0)。重边自环不影响(union 返回 false 直接跳过)。
</details>
题 3:Kruskal 中,为什么 union 返回 false 的边要跳过?如果图不连通,Kruskal 结束后 components 是多少?
union 返回 false 说明两端点已连通,加入这条边会成环,MST 不允许环,故跳过。图不连通时 Kruskal 只能得到最小生成森林,结束后 components = 原图连通块数(>1),此时不存在 MST,只能求最小生成森林。
</details>