并查集

本文针对 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\) 的那些条件是否满足。

由于值域较大,需要离散化。

AC 代码提交记录

#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

AC 代码提交记录

#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 即可,记得特判。

AC 代码提交记录

#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)\ )\)

AC 代码提交记录

#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;
}
posted @ 2023-10-13 21:38  LittleDrinks  阅读(20)  评论(0编辑  收藏  举报