目录
[1. 认识扩展域并查集](#1. 认识扩展域并查集)
[2. 具体示例操作](#2. 具体示例操作)
[1. P1892 团伙 - 洛谷](#1. P1892 团伙 - 洛谷)
[2. P2024 食物链 - 洛谷](#2. P2024 食物链 - 洛谷)
一、什么是扩展域并查集?
1. 认识扩展域并查集
在上一篇文章【算法】数据结构_并查集-CSDN博客 中,我们所介绍的只是普通的并查集,在普通的并查集中,我们认为只要有关系就会将它们都放在同一个集合中,但如果各元素之间存在多种的具体关系,普通并查集就无法解决。
比如:a 和 b 是敌人关系, b 和 c 是敌人关系,但是 a 和 c 其实不是敌人关系,而是另一种朋友关系(即敌人的敌人是朋友)。如果要求集合中全是朋友的集合个数,若我们使用普通并查集时,a,b,c各自就是就是一个集合,共三个集合,但是由于a和c是朋友关系,所以正确答案应该是2个集合,即bc一个集合,a一个集合。
解决这类问题就需要对并查集进行扩展(即扩展域并查集):将每个元素拆分成多个域,每个域代表一种状态或者关系。通过维护这些域之间的关系,来处理复杂的约束条件。
2. 具体示例操作
在敌人朋友问题中,如果一共有 n 个人,则我们会将 x 分成两个域(1~n是朋友域,n~2*n是敌人域),朋友域 x 以及敌人域 x + n ,即:
那么对于两个元素 x 和 y :
- 若 x 和 y 是朋友,正常处理,把 x 和 y 合并成一个集合;
- 若 x 和 y 是敌人:那么x和y 的敌人 y+n就是朋友,合并 x 与 y+n ;y 和x 的敌 x+n就是朋友,合并 y 与 x+n 。
这样就可以利用两个域,将所有的关系维护起来。即:
具体示例:
加入这里一共10个元素,我们知道 1 和 2 是敌人,2 和 3 是敌人。则可以得到如图所示的集合信息:
其实扩展域其实就是为我们初始的元素提供了一个联系其他元素的桥梁。
以上就是对朋友和敌人关系的一个问题的理解。同理对于其他关系的题型,也是这样,首先要分析元素之间有几种关系,然后扩展并查集,明确有几种域,根据x和y的关系,进行对应的并查集操作。
要正真理解开展并查集,还是得看看一下例题。
二、扩展并查集的例题
1. P1892 团伙 - 洛谷
题目链接:P1892 BalticOI 2003 团伙 (Day 2) - 洛谷
问题内容:
题目分析:
这道题就是多关系的问题,它们的关系为朋友和敌人的关系,满足一个人的朋友的朋友是朋友,一个人的敌人的敌人是朋友。我们要求的则是一个集合中元素全是朋友的集合个数。
解决方法:利用扩展域并查集解决问题
首先分析这道题的元素的关系:两个元素要么是朋友要么是敌人。一共有n个元素,所以我们可以将x这个元拆分为两个域:朋友域(x),敌人域(x + n)。
然后根据输入的关系维护并查集:
- 如果 p 和 q 是朋友,则让 p 和 q 直接合并。注意:p + n 和 q + n 不需要合并,因为题目没有说朋友的敌人是敌人。
- 如果 p 和 q 是敌人,则p和q 的敌人 q+n就是朋友,合并 p 与 q+n ;q 和p 的敌 p+n就是朋友,合并 q 与 p+n 。
最后,统计一个有多少个集合:维护完并查集后,其中的集合个数就是全是朋友的团体个数,我们这里统计的只是前n个元素所在的集合个数,后面的 n 个集合是我们扩展出来的,只是为了帮助我们连接前 n 个集合。注意:为了保证只统计前n个集合就可以统计出最终集合数,所以我们在合并元素时,必须要要以前n个元素作为根节点,不能以扩展出来的元素作为根节点。
得如图所示:
所以代码实现为:
cpp
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int fa[N * 2]; // 扩展域并查集
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
// 以朋友域的 x 为父
void un(int x, int y)
{
fa[find(y)] = find(x);
}
int main()
{
cin >> n >> m;
// 初始化
for(int i = 1; i <= n * 2; i++) fa[i] = i;
while(m--)
{
char opt; int p, q;
cin >> opt >> p >> q;
if(opt == 'F') // 是朋友
{
un(p, q);
}
else // 是敌人
{
// 以p,q为父进行合并
un(p, q + n);
un(q, p + n);
}
}
// 统计
int ret = 0;
for(int i = 1; i <= n; i++) if(fa[i] == i) ret++;
cout << ret << endl;
return 0;
}
2. P2024 食物链 - 洛谷
问题内容:
解决方法:
从前往后遍历每一句话,判断是否与之前的话矛盾:
- 若矛盾,则舍去这句话,并统计个数;
- 若不矛盾,则通过扩展域并查集维护这句话。
如何维护这句话?
扩展域并查集维护。首先明确元素之前的关系,有题意可以知,因为这里只有A 吃 B,B 吃 C,C 吃 A的关系,且每个动物都是 A,B,C 中的一种,所以任意两个元素 x 和 y 之前都有三种关系:
- x 和 y 是同类;
- x 捕食 y ;
- x 被 y 捕食;
所以对于一个元素 x 可以将它分成三个域:
- 同类域 x (与x在同一个集合的元素和x都是同类);
- 捕食域 x + n (与x + n在同一个集合的元素都能被 x 吃掉);
- 被捕食域 x + 2 * n (与 x + 2 * n 在同一个集合的元素都可以吃掉 x )。
在题中的话只有两种情况,则它们对应的操作如下:
- 如果 x 和 y 是同类,则它们的所有捕食关系是一样的,所以就需要合并x与y,合并x+n和y+n,合并x+2*n和y+2*n。
- 如果 x 可以吃 y,则y的被捕食域(y+2n所在的集合)和 x 就是同类,即合并(x, y+2n);x 的捕食域(x+n所在的集合)和 y 就是同类,即合并(x+n,y);x的被捕食域所在集合中和y捕食域所在的集合就是同类,则合并(y+n,x+2n)。
如图所示;
如何判断是否与之前的话矛盾?
- 如果 x 和 y 是同类,则可以判断 x 是否可以吃 y 以及 y 是否可以吃 x,若其中一个成立则就矛盾;
- 如果 x 可以吃 y,则可以判断x 和 y 是否是同类 以及y 是否可以吃 x ,若其中一个成立则就矛盾。
代码实现:
cpp
#include <iostream>
using namespace std;
const int N = 5e4 + 10;
int fa[N * 3];
int n, k;
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void un(int x, int y)
{
fa[find(x)] = find(y);
}
int main()
{
cin >> n >> k;
// 初始化
for(int i = 1; i <= 3 * n; i++) fa[i] = i;
int ret = 0;
while(k--)
{
int op, x, y;
cin >> op >> x >> y;
if(x > n || y > n) ret++; // 假话
else if(op == 1) // 是同类
{
// 判断矛盾:x->y, y->x
if(find(x + n) == find(y) || find(y + n) == find(x)) ret++;
else
{
// 合并
un(x, y);
un(x + n, y + n);
un(x + 2 * n, y + 2 * n);
}
}
else if(op == 2) // x 吃 y
{
// 判断矛盾:x和y是同类,y -> x
if(find(x) == find(y) || find(y + n) == find(x)) ret++;
else
{
// 合并
un(x, y + 2 * n);
un(y, x + n);
un(y + n, x + 2 * n);
}
}
}
cout << ret << endl;
return 0;
}
感谢各位观看!希望大家多多支持!