来源:算法讲解057【必备】并查集-下 · 2026-10-22
依据:视频字幕 2000 字
并查集(DSU)用“集合标签/集合属性”把同行同列、同属性、同连通块的元素合并,答案往往是 N - 集合个数 或“每个集合维护一个聚合量”。
sz、集合内石头数、集合内最大行号等。合并时把两个标签合并。fa[1e9],要用 unordered_map 或离散化把“行号/列号”映射成编号。以“移除最多同行或同列石头”为例:
k 个连通块,最多删到每个连通块剩 1 个,即剩 k 个,最多删除 N - k。合并方式:不需要两两枚举石头。对每一行维护“这一行第一次出现的石头编号”,对每一列同理;新石头到来时,如果该行/列已有石头,就合并。这样每个石头最多触发两次合并。
n 个石头,每个石头有 (x, y)。fa[i] = i,sz[i] = 1,集合数 cnt = n。rowFirst:行号 -> 该行第一个石头编号;colFirst:列号 -> 该列第一个石头编号。i: - 若 rowFirst 中已有行号 x,设对方为 j,union(i, j);否则 rowFirst[x] = i。
- 若 colFirst 中已有列号 y,设对方为 j,union(i, j);否则 colFirst[y] = i。
cnt--。n - cnt(最多删除数),或 cnt(最少剩余数)。石头:(0,0), (0,1), (1,1), (2,2),n=4。
| 步骤 | 石头 | 行映射 | 列映射 | 合并 | cnt |
|---|---|---|---|---|---|
| 初始 | - | 空 | 空 | - | 4 |
| 1 | (0,0) | row[0]=0 | col[0]=0 | 无 | 4 |
| 2 | (0,1) | row[0]已有0 | col[1]=1 | union(1,0) | 3 |
| 3 | (1,1) | row[1]=2 | col[1]已有1 | union(2,1) | 2 |
| 4 | (2,2) | row[2]=3 | col[2]=3 | 无 | 2 |
最终 cnt=2,最多删除 4-2=2。两个连通块:{0,1,2} 和 {3},各剩 1 个。
class DSU:
def __init__(self, n):
self.fa = list(range(n))
self.sz = [1] * n
self.cnt = n # 集合个数
def find(self, x):
while self.fa[x] != x:
self.fa[x] = self.fa[self.fa[x]]
x = self.fa[x]
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False
if self.sz[ra] < self.sz[rb]:
ra, rb = rb, ra
self.fa[rb] = ra
self.sz[ra] += self.sz[rb]
self.cnt -= 1
return True
def remove_stones(stones):
n = len(stones)
dsu = DSU(n)
row_first = {}
col_first = {}
for i, (x, y) in enumerate(stones):
if x in row_first:
dsu.union(i, row_first[x])
else:
row_first[x] = i
if y in col_first:
dsu.union(i, col_first[y])
else:
col_first[y] = i
return n - dsu.cnt
#include <bits/stdc++.h>
using namespace std;
struct DSU {
vector<int> fa, sz;
int cnt; // 当前集合个数
DSU(int n = 0) { init(n); }
void init(int n) {
fa.resize(n);
sz.assign(n, 1);
iota(fa.begin(), fa.end(), 0);
cnt = n;
}
int find(int x) {
while (fa[x] != x) {
fa[x] = fa[fa[x]];
x = fa[x];
}
return x;
}
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);
fa[rb] = ra;
sz[ra] += sz[rb];
--cnt;
return true;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> x(n), y(n);
for (int i = 0; i < n; ++i) cin >> x[i] >> y[i];
DSU dsu(n);
unordered_map<int, int> rowFirst, colFirst;
rowFirst.reserve(n * 2);
colFirst.reserve(n * 2);
for (int i = 0; i < n; ++i) {
auto itR = rowFirst.find(x[i]);
if (itR != rowFirst.end()) dsu.unite(i, itR->second);
else rowFirst[x[i]] = i;
auto itC = colFirst.find(y[i]);
if (itC != colFirst.end()) dsu.unite(i, itC->second);
else colFirst[y[i]] = i;
}
cout << n - dsu.cnt << '\n';
return 0;
}
O(α(n)),近似常数。n 次 find/unite,总时间 O(n α(n)),哈希表平均 O(1),最坏哈希冲突可退化,竞赛中通常可接受。O(n)。vector + 数组式并查集比 map 快很多。(u,v) 表示合并,问最终有多少组、每组多少元素。unordered_map 或离散化。struct DSU {
vector<int> fa, sz;
int cnt;
DSU(int n = 0) { init(n); }
void init(int n) {
fa.resize(n);
sz.assign(n, 1);
iota(fa.begin(), fa.end(), 0);
cnt = n;
}
int find(int x) {
while (fa[x] != x) {
fa[x] = fa[fa[x]];
x = fa[x];
}
return x;
}
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);
fa[rb] = ra;
sz[ra] += sz[rb];
--cnt;
return true;
}
bool same(int a, int b) { return find(a) == find(b); }
int size(int x) { return sz[find(x)]; }
};
带集合标签的写法(例如集合内石头数、集合内最大行号):
struct DSU {
vector<int> fa, sz;
vector<int> maxRow; // 集合标签示例
int cnt;
void init(int n, const vector<int>& row) {
fa.resize(n); sz.assign(n, 1); maxRow = row;
iota(fa.begin(), fa.end(), 0);
cnt = n;
}
int find(int x) {
while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; }
return x;
}
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);
fa[rb] = ra;
sz[ra] += sz[rb];
maxRow[ra] = max(maxRow[ra], maxRow[rb]); // 合并标签
--cnt;
return true;
}
};
fa[1e9],必须 unordered_map 或离散化。fa/sz/cnt/rowFirst/colFirst 每次都要重置;unordered_map 建议重新构造或 clear()。n - 集合个数,不是 n - 1;空输入/n=0 要特判。long long:答案 n - cnt 在 n 很大时仍可能超 int(若题目规模到 1e9 级别),但本题 n 通常 1e3~1e5,int 够;保险可用 long long 输出。unordered_map<int,int> 在极端数据下可能被卡,竞赛可用 gp_hash_table 或离散化。fa[x] = fa[fa[x]] 是路径减半,正确;不要写成 fa[x] = x。dsu.cnt。i,sz[find(i)]。unite 里合并标签,如 maxRow[ra] = max(maxRow[ra], maxRow[rb])。find 时累加。fa,支持历史版本查询。map 存行/列映射,常数大,1e5 可能 TLE;优先 unordered_map 或离散化后数组。unordered_map 没清空,上一组数据污染下一组。find 递归写法在链很长时爆栈;竞赛用迭代路径减半。--cnt,答案偏大。O(1)(阿克曼反函数);路径压缩和按秩合并能否只用一个;如何支持删除(可撤销并查集/离线倒序)。sum/max/min/count,合并时按需合并,是很多“集合属性”题的核心。题 1:n=3 个石头 (0,0),(0,0),(1,1),最多删除几个?
两个 (0,0) 同行同列合并,(1,1) 独立。集合数 2,答案 3-2=1。
</details>
题 2:n=5 个石头 (0,0),(0,1),(1,1),(2,2),(2,3),最多删除几个?
(0,0)-(0,1)-(1,1) 一个集合;(2,2)-(2,3) 一个集合。集合数 2,答案 5-2=3。
</details>
题 3:如果题目要求输出“每个连通块内石头数量”,并查集要怎么改?
合并时维护 sz[ra] += sz[rb];最后对每个 i 求 find(i),把 sz[find(i)] 累加或直接输出根节点的 sz。注意只输出根节点(find(i)==i)的 sz。
</details>