用并查集维护强制朋友关系,答案是朋友关系图的连通分量个数;2N 并查集是其紧凑模板写法。
OJ: luogu
题目 ID: P1892
难度:普及+/提高
标签:并查集种类并查集连通分量关系传递pythoncpp
日期: 2026-07-16 18:26
题意
n 个人之间有朋友 F 或敌人 E 关系。朋友的朋友是朋友,敌人的敌人也是朋友。朋友必须在同一团体,求最多能有多少个团体。
思路
本质是:把「必须同团」的人连成图,再数连通分量。每个连通分量是一群被规则强制为朋友的人;让每群单独成团,分量个数就是最大团体数。
写法一:建朋友图 + 并查集数连通分量
更贴近题意,适合作为第一版理解:
- 读到
F a b:在朋友图里连边a -- b(并查集union)。 - 读到
E a b:先记下敌人列表。 - 对每个人,把他的所有敌人两两连边——因为「敌人的敌人是朋友」。
- 统计
1..n的连通分量个数。
Python:
python
import sys
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
# 并查集建「朋友关系图」:同一连通分量 = 必须在同一团伙
parent = list(range(n + 1))
size = [1] * (n + 1)
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
a, b = find(a), find(b)
if a == b:
return
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
# 先记下每个人的敌人列表;朋友直接连边
enemies = [[] for _ in range(n + 1)]
pos = 2
for _ in range(m):
relation = data[pos]
a, b = int(data[pos + 1]), int(data[pos + 2])
pos += 3
if relation == b"F":
union(a, b) # 朋友:图中连一条边
else:
enemies[a].append(b)
enemies[b].append(a)
# 敌人的敌人是朋友:同一个人的所有敌人两两连边
for person in range(1, n + 1):
es = enemies[person]
if len(es) < 2:
continue
first = es[0]
for other in es[1:]:
union(first, other)
# 连通分量个数 = 最多团伙数
print(len({find(i) for i in range(1, n + 1)}))
if __name__ == "__main__":
main()C++:
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:44
* update_at: 2026-07-19 11:44
*/
// main2.cpp:建朋友关系图 + 并查集数连通分量(教学版)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
int parent[MAXN];
int sz[MAXN];
vector<int> enemies[MAXN]; // enemies[i]:i 的直接敌人列表
int find_root(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void unite(int a, int b) {
a = find_root(a);
b = find_root(b);
if (a == b) {
return;
}
if (sz[a] < sz[b]) {
swap(a, b);
}
parent[b] = a;
sz[a] += sz[b];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
parent[i] = i;
sz[i] = 1;
}
for (int i = 1; i <= m; i++) {
char op;
int a, b;
cin >> op >> a >> b;
if (op == 'F') {
// 朋友:图中直接连边
unite(a, b);
} else {
// 敌人:先记下来,后面统一处理
enemies[a].push_back(b);
enemies[b].push_back(a);
}
}
// 敌人的敌人是朋友:同一个人的所有敌人两两并到同一集合
for (int person = 1; person <= n; person++) {
int cnt = (int)enemies[person].size();
if (cnt < 2) {
continue;
}
int first = enemies[person][0];
for (int j = 1; j < cnt; j++) {
unite(first, enemies[person][j]);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (find_root(i) == i) {
ans++;
}
}
cout << ans << '\n';
return 0;
}写法二:2N 并查集(正式提交 / 模板写法)
为每个人建立对立侧节点:
x表示人物x;x+n表示与x对立的一侧。
规则:
F a b:只合并a与b;E a b:合并a与b+n,以及b与a+n。
若两人有共同敌人,会通过对立侧落到同一集合,自动得到「敌人的敌人是朋友」。最后只统计真实人物 1..n 的不同代表元。
注意:朋友关系时不要再合并 a+n 与 b+n。多合并对立侧会把不该同团的人连起来,答案偏小(容易只得部分分)。
Python 知识
- 并查集用列表
parent/size,循环路径压缩不受递归深度限制。 {find(i) for i in range(1, n + 1)}用集合推导式统计连通分量。- 关系 token 可保留为
bytes,直接与b"F"比较。 - 敌人列表
enemies[i]是「按点存邻接」,再统一连边。 /home/rainboy/mycode/hugo-blog/content/program_language/python/collections_toolkit.md:集合推导式与去重。/home/rainboy/mycode/hugo-blog/content/program_language/python/cpp_to_python_pitfalls.md:迭代替代深递归。
代码
正式提交用 2N 并查集。
Python:
python
import sys
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
# x: 人物 x;x+n: 与 x 对立的一侧
parent = list(range(2 * n + 1))
size = [1] * (2 * n + 1)
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a, b):
a, b = find(a), find(b)
if a == b:
return
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
pos = 2
for _ in range(m):
relation = data[pos]
a, b = int(data[pos + 1]), int(data[pos + 2])
pos += 3
if relation == b"F":
# 朋友:只合并真人,不要合并对立侧
union(a, b)
else:
# 敌人:a 与 b 的对立侧同组,b 与 a 的对立侧同组
# 从而「有共同敌人」的人会落到同一集合
union(a, b + n)
union(b, a + n)
print(len({find(person) for person in range(1, n + 1)}))
if __name__ == "__main__":
main()C++:
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:44
* update_at: 2026-07-19 11:44
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
int parent[MAXN * 2]; // x: 真人 x;x+n: x 的对立侧
int sz[MAXN * 2];
int find_root(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
// 按秩(按大小)合并
void unite(int a, int b) {
a = find_root(a);
b = find_root(b);
if (a == b) {
return;
}
if (sz[a] < sz[b]) {
swap(a, b);
}
parent[b] = a;
sz[a] += sz[b];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= 2 * n; i++) {
parent[i] = i;
sz[i] = 1;
}
for (int i = 1; i <= m; i++) {
char op;
int a, b;
cin >> op >> a >> b;
if (op == 'F') {
// 朋友:只合并真人
unite(a, b);
} else {
// 敌人:a 与 b 的对立侧同组,b 与 a 的对立侧同组
unite(a, b + n);
unite(b, a + n);
}
}
// 统计 1..n 的连通分量个数
int ans = 0;
for (int i = 1; i <= n; i++) {
if (find_root(i) == i) {
ans++;
}
}
cout << ans << '\n';
return 0;
}复杂度
- 写法一:处理敌人两两连边时,每人敌人很少,总复杂度仍可视为
量级(本题 足够)。 - 写法二:每条关系常数次合并,时间
,空间 。
总结
这题正解就是并查集。先想成「建强制朋友关系图,数连通分量」;再压缩成 2N 种类并查集模板。
main2.py/main2.cpp:教学版(敌人列表 + 连通分量)main.py/main.cpp:正式版(2N 并查集)