并查集
本文针对 CSP-S2/NOIP 复习,重点在在哪用、怎么写,底层原理和实现不是重点。
并查集的概念、写法
【三种并查集】
- 朴素并查集:用于维护动态连通性,给出点与点是否连通。
- 种类并查集:用于维护“种类”,相较上一种,可以维护“敌人的敌人是朋友”这种关系。
- 带权并查集:在朴素并查集的基础上,给边加上权值。
【写法】
这是我常用的板子,开了路径压缩。
int f[MAXN];
int find(int x)
{
return f[x]==x? x: f[x]=find(f[x]);
}
void merge(int x, int y)
{
f[find(x)] = find(y);
}
写题的时候写错很多遍的点:
find()
函数中,f[x]=
老是漏掉,也就是没有路径压缩,导致 TLE;merge()
函数中,f[find()]=find()
,两个find()
都不能省:- 省去第一个
find()
,修改的不是这个连通块的根节点; - 省去第二个
find()
,连接到的不是连通块的根节点,有可能出现f[x]=y, f[y]=x
的情况,导致下次find()
时死循环、爆栈 RE;
- 省去第一个
- 没有初始化,一开始应该令
f[i]=i
。
例题
题目 | 备注 |
---|---|
P1955 [NOI2015] 程序自动分析 | 朴素并查集,需要离散化 |
[ABC120D] Decayed Bridges | 朴素并查集,需要额外维护连通块大小 |
P2024 [NOI2001] 食物链 | 种类并查集,也能用带权并查集写,三个种类 |
iai28 五行学说 | 同上,但有五个种类 |
P1955 [NOI2015] 程序自动分析
先做 \(e=1\) 的约束条件,再判断 \(e=0\) 的那些条件是否满足。
由于值域较大,需要离散化。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int t, f[MAXN<<1], id_to_ij[MAXN<<1], cp0, cp1, cnt;
struct node {
int x, y;
} p0[MAXN], p1[MAXN];
int find(int x)
{
return f[x]==x? x: f[x]=find(f[x]);
}
void ij_to_id(int &x)
{
x = lower_bound(id_to_ij+1, id_to_ij+cnt+1, x)-id_to_ij;
}
int main()
{
// freopen("P1955_2.in", "r", stdin);
cin >> t;
while (t--) {
cp0 = cp1 = cnt = 0;
int n; cin >> n;
for (int i = 1; i <= 2*n; ++i) { f[i] = i; }
for (int i = 1; i <= n; ++i) {
int x, y, e;
cin >> x >> y >> e;
id_to_ij[++cnt] = x;
id_to_ij[++cnt] = y;
if (e == 0) { p0[++cp0] = { x, y }; }
else { p1[++cp1] = { x, y }; }
}
sort(id_to_ij+1, id_to_ij+cnt+1);
cnt=unique(id_to_ij+1, id_to_ij+cnt+1)-id_to_ij-1;
for (int i = 1; i <= cp1; ++i) {
int x=p1[i].x, y=p1[i].y;
ij_to_id(x);
ij_to_id(y);
if (find(x) != find(y)) { f[find(x)] = find(y); }
}
bool ava=true;
for (int i = 1; i <= cp0; ++i) {
int x=p0[i].x, y=p0[i].y;
ij_to_id(x);
ij_to_id(y);
if (find(x) == find(y)) { ava=false; cout << "NO" << endl; break; }
}
if (ava) { cout << "YES" << endl; }
}
return 0;
}
[ABC120D] Decayed Bridges
用并查集维护所有的连通块,\(x\) 所在连通块的大小记作 \(sz_x\)。每次把 \(x\) 和 \(y\) 断开时:
- 如果 \(x\) 和 \(y\) 在同一个连通块内,答案不变;
- 否则,可以从第 \(x\) 和第 \(y\) 个连通块内分别挑出一个点 \(i,j\),满足 \(D(i,j)=1\),计入答案。
反向观察所有的删边操作。对于 \(n\) 个点的图,在一开始一条边都没有的情况下,\(ans=C_n^2\)。每次把删的边加回去,并且如果一条边两个端点 \(x\) 和 \(y\) 不在一个连通块内,答案减去 \(sz_x\times sz_y\)。
注意在计算 ans
的时候要开 long long
,\(n\) 或 \(sz\) 如果用 int
存,计算时要先乘上 1LL
。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
int n, m, fr[MAXN], to[MAXN], f[MAXN], sz[MAXN];
ll tmp, ans[MAXN];
int find(int x)
{
return f[x]==x? x: f[x]=find(f[x]);
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
cin >> fr[i] >> to[i];
}
for (int i = 1; i <= n; ++i) {
f[i] = i;
sz[i] = 1;
}
tmp = 1LL*n*(n-1)/2;
for (int i = m; i; --i) {
ans[i] = tmp;
int fu=find(fr[i]), fv=find(to[i]);
if (fu != fv) {
tmp -= 1LL * sz[fv] * sz[fu];
sz[fv] += sz[fu];
sz[fu] = 0;
f[fu] = fv;
}
}
for (int i = 1; i <= m; ++i) {
cout << ans[i] << endl;
}
return 0;
}
P2024 [NOI2001] 食物链
在并查集中,我们通过指定 组长
的方式来规定一个组,把成员当做了一个明确的类别,也就决定了普通的并查集只能维护 朋友的朋友是朋友
,即谁和谁是一类的,而不能维护 敌人的敌人
这层关系。
想要维护 敌人的敌人
这层关系,可以使用种类并查集。
我们可以独立放置一个假想的敌人,用这个假想的敌人在并查集中 明确一个类别
,进而维护住答案。
具体而言,设 \(find(x)\) 是并查集查找的结果。如果一个动物的编号是 \(A\),我们定义编号为 \(A+n\) 的动物是 \(A\) 的食物,而 \(A+2n\) 的动物是 \(A\) 的天敌。每当遇到一句结论 a x y
:
- 如果 \(find(x)=find(y)\),则两者是朋友关系;
- 如果 \(find(x+n)=find(y)\),即 \(y\) 和 \(x\) 的食物是一类,即 \(x\) 捕食 \(y\);
- 如果 \(find(x)=find(y+n)\),同理,是 \(y\) 捕食 \(x\)。
按照这种维护方式遍历所有结论 a x y
即可,记得特判。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=5e4+5, MAXK=1e5+5;
int n, k, f[MAXN<<2], ans;
int find(int x)
{
return f[x]==x? x: find(f[x]);
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= 3*n; ++i) {
f[i] = i;
}
for (int i = 1; i <= k; ++i) {
int a, x, y;
cin >> a >> x >> y;
if (x>n || y>n) {
++ans; continue;
}
if (a == 1) {
if (find(x)==find(y+n) || find(x+n)==find(y)) { ++ans; }
else {
f[find(x)] = find(y);
f[find(x+n)] = find(y+n);
f[find(x+2*n)] = find(y+2*n);
}
} else {
if (x==y || find(x)==find(y) || find(y+n)==find(x)) { ++ans; }
else {
f[find(x+n)] = find(y);
f[find(x+2*n)] = find(y+n);
f[find(x)] = find(y+2*n);
}
}
}
cout << ans << endl;
return 0;
}
iai28 五行学说
同 P2024,只不过这里有五类。
用 \(f[x+i\times n]\) 表示 \(x\) 所属的属性在图上向后 \(i\) 位的那一种元素。
比如一共有 \(5\) 个物品,如果第 \(3\) 个物品的属性是火,在并查集中,\(f[3]\) 表示火,\(f[3+5]\) 表示土,\(f[3+2\times5]\) 表示金,……。
由题意可知,\(f[x]\) 会生 \(f[x+n]\),会克 \(f[x+2\times n]\)。对应的合并操作如下:
- 如果观察为
s
,那么需要分别合并 \((\ x+n\times((i+1)\%5),\ y+n\times (i\%5)\ )\)。 - 如果观察为
k
,那么需要分别合并 \((\ x+n\times((i+2)\%5),\ y+n\times (i\%5)\ )\)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n, m, f[MAXN*5], ans;
int find(int x)
{
return f[x]==x? x: f[x]=find(f[x]);
}
bool check(int x, int y, int expx)
{
for (int i = 0; i <= 4; ++i) {
if ( (i != expx) && (find(x+n*i)==find(y)) ) { return false; }
}
return true;
}
void merge(int x, int y, int delta)
{
for (int i = 0; i <= 4; ++i) {
f[find(x+n*((i+delta)%5))] = find(y+n*i);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n*5; ++i) {
f[i] = i;
}
for (int i = 1; i <= m; ++i) {
char t;
int x, y;
cin >> t >> x >> y;
if (t == 's') {
if (check(x,y,1)) {
merge(x,y,1);
} else {
++ans;
}
} else {
if (check(x,y,2)) {
merge(x,y,2);
} else {
++ans;
}
}
}
cout << ans << endl;
return 0;
}