@[toc]

简介

并查集是用于处理点集之间关系的一种方法,支持合并和查询两种操作,即将两个集合合并和查询两个点是否位于同一集合内,是一种树形数据结构,一般采用数组实现。
首先定义一个数组$f$,对于点$i$,$f[i]$存储着i的父节点的下标,通过递归查找找到其根节点。而合并点$i,j$时,首先找到$i,j$的父节点$pi,pj$,然后让$f[pi]=pj$或$f[pj]=pi$即可,也就是让$i,j$所在的集合的根节点同一。查询时只要查询两个点的根节点是否一样即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int f[100];//存储父节点
void ini()//初始化
{
for (int i = 1; i <= 100; i++)
f[i] = i;//一开始节点i根节点当然是i啦
}
int find(int i)
{
if (i == f[i])//如果i的父节点使它本身,说明到头了,i就是根节点
return i;
return find(f[i]);//没到头就继续往下找
}
void merge(int i, int j)
{
f[find(i)] = find(j);/*合并直接让一个集合的根节点连向另一个根节点就行,毕竟
根节点就是一个集合的标志*/
}
bool query(int i, int j)//查找其根节点是否一致即可
{
return find(i) == find(j);
}

路径压缩

在并查集中,我们关心的只是该节点的根节点,在多次合并后会出现一条一条的链在数组中,查询要从叶节点走到根节点。那既然我们不关心路径,只关心根节点,那让所有的非根节点直接指向根节点,那查询时岂不是一步到位!这就是路径压缩。具体实现看代码

1
2
3
4
5
6
7
int find(int i)
{
if (i == f[i])
return i;
return (fd[i] = find(i));/*这里只多了一步,既然find最终返回的是根节点,那
寻找时让路上的节点等于find就行了。*/
}

集合数量记录

对于点的编号完全大于0,每个数组保存它的父节点,那根节点呢?其实根节点不一定时指向自己,它可以是一个负数,当$find()$到负数是就返回,所以根节点的数组可以用来记录额外的信息,比如集合数量。我们定义集合数量=$-f[根节点标号]$。具体实现看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int f[100];//存储父节点
void ini()//初始化
{
for (int i = 1; i <= 100; i++)
f[i] = -1;//一开始每个集合只有一个点,几位-1
}
int find(int i)
{
if (f[i] < 0)//如果记录的是负数,那就到头了
return i;
return find(f[i]);//没到头就继续往下找
}
void merge(int i, int j)
{
f[find(j)]+=f[find(i)];//更新规模
f[find(i)] = find(j);//合并
}
int query(int i)//查询点i所在集合的点数量
{
return -f[find(i)];
}

Q1并查集【模板】

原题传送门->洛谷P3367
典型模板,给你m个操作,有合并,有查询,输出查询结果即可。

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
#include<iostream>
#include<vector>
#include<climits>
#include<string>
#include<queue>
#include<functional>
#include<set>
#include<algorithm>
#include<cmath>
#include<iomanip>
using namespace std;
int f[10001];
void iui()
{
for (int i = 1; i <= 10000; i++)
f[i] = i;
}
int find(int x)
{
return f[x] == x ? x : (f[x] = find(f[x]));
}
void merge(int i, int j)
{
f[find(i)] = find(j);
}
bool query(int i, int j)
{
return find(i) == find(j);
}
int main()
{
iui();
int n, m;
cin >> n >> m;
while (m--)
{
int op, i, j;
cin >> op >> i >> j;
if (op == 1)merge(i, j);
else
{
if (query(i, j))
cout << 'Y' << endl;
else
cout << 'N' << endl;
}
}
return 0;
}

Q2修复公路

原题传送门->洛谷P1111
每修复一条道路实际上就是将道路两端的村庄所在的集合合并,应为两个联通块联通了,典型的并查集操作。题目要求输出最早时间,只要按时间从早到晚将边排序,一条一条合并,判断联通快数量即可。具体实现看代码。

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<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n, m;
int f[1001];//并查集数组
struct E{
int x, y, t;//道路两端节点编号和时间
}edge[100001];
int find(int x)
{
if (x == f[x])return x;
return f[x] = find(f[x]);
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= n; i++)//初始化并查集
f[i] = i;
for (int i = 1; i <= m; i++)
cin >> edge[i].x >> edge[i].y >> edge[i].t;
sort(edge + 1, edge + m + 1, [](E&l, E&r)//排序
{
return l.t < r.t;
});
int ans = -1;//初始化为-1,答案未更新即联不通,到时直接输出
for (int i = 1; i <= m; i++)//从早到晚枚举
{
E now = edge[i];
int x = find(now.x), y = find(now.y);
if (x != y)//若根节点不同
{
f[x] = y;//合并
n--;//联通块数减一(刚开始n个节点独立,联通块数为1)
}
if (n == 1)//如果只剩一个联通块(图已联通)
{
ans = now.t;//答案更新
break;
}
}
cout << ans;//直接输出
return 0;
}

Q3星球大战

原题传送门->洛谷P1197
一看题目,不断摧毁隧道,这是不断分离,而非合并,那咋办嘞?其实分离操作反过来不就是合并吗?所以我们从最后的道路被摧毁开始,往前一步一步修建隧道,再逆序输出答案即可。具体实现看代码

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
70
71
72
73
74
75
76
77
78
#include<iostream>
#include<vector>
#include<climits>
#include<string>
#include<queue>
#include<functional>
#include<set>
#include<algorithm>
#include<cmath>
#include<iomanip>
using namespace std;
const int MAX = 400001;
int n, m;
int f[MAX];//并查集
vector<int> node[MAX];//图
bool broken[MAX];//标记是否被损坏
int point[MAX];//记录损坏点
int find(int x)
{
return (f[x] == x) ? x : (f[x] = find(f[x]));
}
int main()
{
ios::sync_with_stdio(false);
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++)//初始化
f[i] = i;
for (int i = 0; i < m; i++)//建图
{
int x, y; cin >> x >> y;
node[x].push_back(y);
node[y].push_back(x);
}
int k; cin >> k;//k次损坏
for (int i = 0; i < k; i++)//记录损坏的点
{
int x; cin >> x;
broken[x] = true;
point[i] = x;
}
int ans = n - k;//一开始点独立,反叛军有n-k个占领点(从后往前)
for (int i = 0; i < n; i++)
{
if (!broken[i])
{
for(auto e:no/de[i])

if (!broken[e]&&find(e)!=find(i))/*如果是叛军掌握的两
个点并且还没合并*/
{
ans--;//联通块数减一
f[find(i)] = find(e);//合并
}
}
}
point[k] = ans;//point数组的损坏点逆序使用,所以用过后可以用来记录答案
for (int i = k - 1; i >= 0; i--)//从k-1到0共k个点
{
int now = point[i];//弹出
broken[now] = false;//标记为未损坏
ans++;//联通块加一(有一个点进来了)
for (auto e : node[now])//枚举该点所有连边
{
if (!broken[e]&&find(now)!=find(e))/*如果有点与他相连且没
坏且还未合并*/
{
f[find(now)] = find(e);//合并
ans--;//联通块数减一
}
}
point[i] = ans;//逆序记录答案

}
for (int i = 0; i <= k; i++)//输出
cout << point[i] << endl;
return 0;
}

种类并查集

一种特殊的并查集,通过扩大n倍大小来表示n种种类关系间的集合。通过一个例子来讲会比较清晰

Q4关押罪犯

原题传送门->洛谷p1525
这道题题目要求将罪犯关再两个监狱,即两个集合,两种关系,而不是单一的一种关系了。如果罪犯i,j有怨气,要将他们关在两个不同的监狱,这怎么实现呢?
首先有监狱A监狱B两个监狱,我们开两倍的并查集数组,前1~n表示A监狱,n+1
~2n表示监狱B。关押罪犯i,j时,只要$p[find(i+n)]=find(j)$,$p[find(j+n)]=find(i)$,也就是B监狱关押i,A监狱关押j,将这个关系并入一个集合,同时A监狱关押i,B监狱关押j也并入一个集合。查询时只要监狱A同时出现i,j或监狱B同时出现i,j说明关着关着i,j关在了一起,即$find(i)==find(j)||find(i+n)==find(j+n)$。这就是种类并查集。
而这道题也是个贪心,将矛盾关系以怨气值从小到大排列,一条一条关押,合并,直到关押某一对罪犯时发现一关押他们就在同一监狱出现,这就是最小怨气值了,输出即可。具体看代码

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
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX = 20001;
int n, m;
int f[MAX * 2];//开二被数组
struct rel {
int x, y, val;//关系集,罪犯x和y有val的怨气
}edge[100001];
int find(int x)
{
return f[x] == x ? x : (f[x] = find(f[x]));
}
int main()
{
ios::sync_with_stdio(false);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
cin >> edge[i].x >> edge[i].y >> edge[i].val;
for (int i = 1; i <= n * 2; i++)//初始化
f[i] = i;
sort(edge + 1, edge + 1 + m, [](rel&l, rel&r)//排序
{
return l.val > r.val;
});
for (int i = 1; i <= m; i++)
{
int x = edge[i].x, y = edge[i].y;
if (find(x)==find(y)||find(x+n)==find(y+n))//如果在同一监狱就直接输出
{//并推出main
cout << edge[i].val;
return 0;
}
f[find(x + n)] = find(y);//否则就建立关系
f[find(y + n)] = find(x);
}
cout << 0;//如果和平解决就输出0
return 0;
}

Q5食物链

原题传送门->洛谷P2024
这到题的物种有三种关系(环形食物链),即同类,猎物,天敌,物种i,j的关系
只有这三种。这里我将1到n表示同类,1+n到2n表示猎物,2n+1到3n表示天敌。对于操作1,只要物种i,j就没有猎物关系也没有天敌关系就是真话(先不管后两个条件),同类关系的建立需要:

1
2
3
f[find(x)] = find(y);
f[find(x+n)] = find(y+n);
f[find(x+2*n)] = find(y+2*n);

也就是同类之间具有相同的同类,相同的猎物,相同的天敌更新三种关系(同类,猎物,天敌)。对于操作2,只要XY既不是同类也没有X是Y的猎物关系,那就是真话。既然X吃Y,关系建立只需要建立X的猎物是Y,X的天敌是Y的猎物,X的同类是Y的天敌,即三种关系都更新一下(同类,猎物,天敌)。

1
2
3
f[find(x + n)] = find(y);//X的猎物是Y
f[find(x + 2 * n)] = find(y + n);//X的天敌是Y的猎物
f[find(x)] = find(y + 2 * n);//X的同类是Y的天敌

这就是这道题的核心,剩下的细节看代码吧

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
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX = 3*(5e4 + 1);
int n, m;
int f[MAX];//并查集
int find(int x)
{
return (f[x] == x) ? x : (f[x] = find(f[x]));
}
int main()
{
ios::sync_with_stdio(false);//
cin >> n >> m;
int ans = 0;//假话数
for (int i = 1; i <= 3*n; i++)
f[i] = i;
int op, x, y;
for (int i = 1; i <= m; i++)
{
cin >> op >> x >> y;
if (x > n || y > n) { ans++; continue; }//条件2
if (op == 2 && x == y) { ans++; continue; }//条件3
if (op == 1)
{//如果X的猎物时Y或X的天敌时Y,就是假话
if (find(x+n) == find(y) || find(x+2*n) == find(y)) { ans++; continue; }
f[find(x)] = find(y);
f[find(x+n)] = find(y+n);
f[find(x+2*n)] = find(y+2*n);
}
if (op == 2)
{//如果X的同类时Y或X的天敌是Y,就是假话
if (find(x) == find(y) || find(x + 2 * n) == find(y)) { ans++; continue; }
f[find(x + n)] = find(y);
f[find(y + n)] = find(x+2*n);
f[find(x)] = find(y + 2 * n);
}
}
cout << ans;//输出答案
return 0;
}