种类并查集
代码模板种类并查集的原理与实现:维护元素间对立关系,处理二分图判定类问题。
一句话算法
种类并查集给每个元素开一个“反面身份”,用合并同类和反类来维护对立关系。
问题模型
有
这和判断图是否为二分图是同一个模型:
- 元素是点;
- “不同类”关系是边;
- 如果出现奇环,就无法分成两类。
核心直觉
普通并查集只能表达“同类”。
为了表达“不同类”,给每个点
x 表示 x 所在类别
x + n 表示 x 的相反类别
如果题目告诉我们 x 和 y 不同类,那么可以推出:
x和y 的反面同类;x 的反面和y同类。
所以执行:
1
2
unite(x, y + n);
unite(x + n, y);
如果在加入这条关系前,已经发现 x 和 y 同类,就产生冲突。
算法步骤
- 建立大小为
2 * n的并查集。 - 对每条“
x与y不同类”的约束:- 如果
same(x, y),说明冲突; - 合并
x与y+n; - 合并
x+n与y。
- 如果
- 所有约束处理完仍无冲突,则关系一致。
算法证明
核心不变量:对每个元素 x+n 表示与
-
约束翻译正确
若
与 不同类,则 必须与 的反类同类, 的反类也必须与 同类。 -
冲突判断正确
如果加入“
与 不同类”前, x和y已在同一集合,则已有约束要求它们同类,新约束要求它们不同类,矛盾。 -
传递关系正确
并查集会自动传递所有同类关系。反集建模把不同类关系也转成同类合并,所以后续冲突能被
same检出。
因此种类并查集能正确判断二元对立关系是否一致。
复杂度分析
设元素数为
- 时间复杂度:
。 - 空间复杂度:
,实际开 2n个并查集节点。
代码实现
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
62
63
64
65
66
67
68
69
#include <bits/stdc++.h>
using namespace std;
// 并查集判二分图:把每个点拆成"本体"和"对立面",矛盾即非二分图
struct DSU {
using T = int;
vector<T> fa; // fa[x] 为 x 的父节点
vector<T> sz; // sz[x] 为集合大小(仅根节点有效)
DSU(int n = 0) {
init(n);
}
void init(int n) {
fa.resize(n + 1);
sz.assign(n + 1, 1);
iota(fa.begin(), fa.end(), 0);
}
// 查询 x 所在集合的根(路径压缩)
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
// x 与 y 是否在同一集合
bool same(int x, int y) {
return find(x) == find(y);
}
// 合并 x 与 y 所在集合(按大小合并)
void unite(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return;
if (sz[fx] < sz[fy]) swap(fx, fy);
fa[fy] = fx;
sz[fx] += sz[fy];
}
};
int main() {
int n, m;
cin >> n >> m;
// 拆点:i 表示点 i,i+n 表示"点 i 的对立面"
DSU dsu(2 * n);
auto enemy = [n](int x) {
return x + n;
};
bool ok = true;
while (m--) {
int x, y;
cin >> x >> y;
// x 与 y 必须在不同类别中。
if (dsu.same(x, y)) {
ok = false;
}
dsu.unite(x, enemy(y));
dsu.unite(enemy(x), y);
}
cout << (ok ? "YES" : "NO") << "\n";
return 0;
}
不用 struct 的精简写法:拆点后 x + n 就是 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
#include <bits/stdc++.h>
using namespace std;
// 种类并查集(并查集判二分图)竞赛精简版:每个点拆成 x 与 x+n(对立面)
// 输入 n m 与 m 条边 x y(x 与 y 必须不同类),输出 YES/NO
// 口诀:要求不同类,就把 x 挂到 y 的对立面、y 挂到 x 的对立面
// 易错点:拆点后容量翻倍,init 要开到 2n;x 与 y 同集合才是矛盾,合并本身不会矛盾
// 和别的模板一起用时 maxn 只留一份
const int maxn = 1e6 + 5;
int fa[maxn * 2], sz[maxn * 2]; // 拆点后下标 1..2n
void init(int n) {
for (int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1;
}
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
// 小树挂大树
void unite(int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) return;
if (sz[fx] < sz[fy]) swap(fx, fy);
fa[fy] = fx;
sz[fx] += sz[fy];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
init(2 * n); // 拆点:i 是点 i,i+n 是它的对立面
bool ok = true;
while (m--) {
int x, y;
cin >> x >> y;
if (find(x) == find(y)) ok = false; // 同类却要求不同类:矛盾
unite(x, y + n); // x 和 y 的对立面同类
unite(x + n, y); // x 的对立面和 y 同类
}
cout << (ok ? "YES" : "NO") << "\n";
return 0;
}
Python 版抽成 DSU 类,is_bipartite 对应 C++ main 的判定逻辑,单文件即可复制使用。
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
# 种类并查集(判二分图):把每个点 x 拆成"本体 x"与"对立面 x+n"。
# 下标约定:点编号 1..n,并查集规模 2n;"x 与 y 异类" 转成合并 (x, y+n) 与 (x+n, y)。
# is_bipartite 对应 C++ main 的判定逻辑;DSU 与 dsu.py 一致,单文件即可复制使用。
class DSU:
"""并查集 (Disjoint Set Union)"""
def __init__(self, n: int):
# 1-indexed, fa 初始指向自己,sz 初始为 1
self.fa = list(range(n + 1))
self.sz = [1] * (n + 1)
def find(self, x: int) -> int:
"""查找根节点,并进行路径压缩"""
if self.fa[x] != x:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
def same(self, x: int, y: int) -> bool:
"""判断 x 和 y 是否属于同一个集合"""
return self.find(x) == self.find(y)
def unite(self, x: int, y: int) -> bool:
"""按大小合并两个集合 (小树挂大树)"""
fx, fy = self.find(x), self.find(y)
if fx == fy:
return False
# 确保 fx 始终是较大的树
if self.sz[fx] < self.sz[fy]:
fx, fy = fy, fx
self.fa[fy] = fx
self.sz[fx] += self.sz[fy]
return True
def is_bipartite(n: int, edges: list[tuple[int, int]]) -> bool:
"""n 个点、给定"必须异类"边集能否二染色;x 与 y 已同类即矛盾。
点编号 1..n。调用示例:is_bipartite(3, [(1, 2), (2, 3), (1, 3)]) -> False
"""
dsu = DSU(2 * n) # 拆点:i 是点 i,i+n 是它的对立面
for x, y in edges:
if dsu.same(x, y):
return False # 已有约束要求同类,新约束要求异类,矛盾
dsu.unite(x, y + n) # x 和 y 的对立面同类
dsu.unite(x + n, y) # x 的对立面和 y 同类
return True
测试用例
输入:
3 3
1 2
2 3
1 3
输出:
NO
解释:三个人两两不同类,需要三种类别,但模型只有两类,所以冲突。
应用分类详解
种类并查集的本质是:把“不同类”这种二元对立关系,转成并查集能维护的“同类”关系。
一、二分图在线判定
- 典型模式: 不断加入“两个点必须不同色”的边,判断当前约束是否还能二染色。
- 识别信号: 题面出现“分成两组”“两端不能同组”“是否存在奇环”。
- 核心建模: 每个点开正反两个身份。边
表示 x和y+n同类,x+n和y同类;若x和y已同类,则出现奇环。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 二分图动态加边 | 二分图判定变体 | 每条边表示两端点颜色相反 |
| 奇环冲突检测 | 常见图染色题 | same(x,y) 时说明新边强迫同色点异色 |
| 性别关系判定 | poj-2492 | 每条关系表示两个个体必须不同类 |
二、敌对关系与分组约束
- 典型模式: 人、动物、任务必须分到两个阵营,某些关系要求两者不能同组。
- 识别信号: 出现“敌人”“不能关在一起”“互斥”“两类阵营”。
- 核心建模: 敌人的敌人可以同组,所以把“敌对”转成反集之间的合并。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 关押罪犯 | luogu-P1525 | 按冲突值从大到小加入敌对关系,第一次冲突就是答案 |
| 两组任务安排 | 分组可行性题 | 任务冲突边要求两端进入不同集合 |
| 社交敌友关系 | 敌人的敌人模型 | 反集维护敌对传递 |
三、带权或离线的对立关系
- 典型模式: 约束有强弱顺序,要找最早冲突、最大可满足前缀或最小不可满足阈值。
- 识别信号: 出现“最大怨气”“按权值排序”“前若干条约束是否可行”。
- 核心建模: 外层排序或二分答案,内层用种类并查集检查一批二元对立关系是否一致。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 最大冲突阈值 | luogu-P1525 | 降序处理边,冲突时当前权值不可避免 |
| 可满足前缀 | 约束一致性题 | 二分前缀长度,用种类并查集判定 |
| 离线加边判矛盾 | 分批关系检查 | 每批关系重建 DSU 或回滚 DSU |
经典例题
1. luogu-P1525
关押罪犯。敌对关系要求两人分到不同监狱,可以用种类并查集维护冲突。
2. poj-2492
A Bug’s Life。给出若干异性关系,判断是否存在冲突。
3. 二分图判定
如果图是在线加边,且每条边表示两端点不同色,可以用种类并查集动态维护是否出现奇环。