上文链接
一、扩展域并查集
普通的并查集只能解决各元素之间仅存在一种相互关系,比如在普通并查集的《亲戚》这道题中:
- a a a 和 b b b 是亲戚关系, b b b 和 c c c 是亲戚关系,这时就可以查找出 a a a 和 c c c 也存在亲戚关系。
但如果存在各元素之间存在多种相互关系,普通并查集就无法解决。比如下面的案例:
- a a a 和 b b b 是敌人关系, b b b 和 c c c 是敌人关系,那么 a a a 和 c c c 的关系是什么呢?有句话说得好:敌人的敌人是朋友。所以 a a a 和 c c c 其实不是敌人关系,而是一种朋友关系。
此时,就不仅仅是简单的敌人关系,还是出现一种朋友关系。
解决这类有多种关系的问题就需要对并查集进行扩展:**将每个元素拆分成多个域,每个域代表一种状态或者关系。**通过维护这些域之间的关系,来处理复杂的约束条件。
比如现在有 n n n 个人,它们之间可能是朋友关系可能是敌人关系,普通并查集我们会开辟一个大小为 n n n 的数组来维护它们之间的关系,现在它们的关系有两种,我们我们可以把数组扩大成两倍,开辟一个大小为 2 n 2n 2n 的数组, [ 1 , n ] [1, n] [1,n] 这个区间表示朋友域, [ n + 1 , 2 n ] [n + 1,2n] [n+1,2n] 区间表示敌人域,接下来:
- 如果 x x x 和 y y y 是朋友( 1 ≤ x , y ≤ n 1 \le x, y \le n 1≤x,y≤n),那么正常处理,把 x x x 和 y y y 合并成一个集合;
- 如果 x x x 和 y y y 是敌人,那么我们把 x x x 和 y + n y + n y+n 合并成一个集合,表示 x x x 和 y y y 是敌人,注意我们也需要把 x + n x + n x+n 和 y y y 合并成一个集合。
这样就可以利用两个域,将两种关系维护起来。
二、OJ 练习
1. 团伙 ⭐⭐⭐
【题目链接】
[P1892 BalticOI 2003] 团伙 - 洛谷
【题目描述】
现在有 n n n 个人,他们之间有两种关系:朋友和敌人。我们知道:
- 一个人的朋友的朋友是朋友
- 一个人的敌人的敌人是朋友
现在要对这些人进行组团。两个人在一个团体内当且仅当这两个人是朋友。请求出这些人中最多可能有的团体数。
【输入格式】
第一行输入一个整数 n n n 代表人数。
第二行输入一个整数 m m m 表示接下来要列出 m m m 个关系。
接下来 m m m 行,每行一个字符 o p t opt opt 和两个整数 p , q p,q p,q,分别代表关系(朋友或敌人),有关系的两个人之中的第一个人和第二个人。其中 o p t opt opt 有两种可能:
- 如果 o p t opt opt 为
F,则表明 p p p 和 q q q 是朋友。- 如果 o p t opt opt 为
E,则表明 p p p 和 q q q 是敌人。
【输出格式】
一行一个整数代表最多的团体数。
【示例一】
输入
6 4 E 1 4 F 3 5 F 4 6 E 1 2输出
3
【说明/提示】
对于 100 % 100\% 100% 的数据, 2 ≤ n ≤ 1000 2 \le n \le 1000 2≤n≤1000, 1 ≤ m ≤ 5000 1 \le m \le 5000 1≤m≤5000, 1 ≤ p , q ≤ n 1 \le p,q \le n 1≤p,q≤n。
(1) 解题思路
开辟一个 2 n + 10 2n + 10 2n+10 大小的数组, [ 1 , n ] [1, n] [1,n] 区间表示朋友域, [ n + 1 , 2 n ] [n + 1, 2n] [n+1,2n] 区间表示敌人域,如果 x x x 和 y y y 是朋友,那么把 x x x 和 y y y 合并成一个集合;如果 x x x 和 y y y 是敌人,那么我们把 x x x 和 y + n y + n y+n 合并成一个集合, x + n x + n x+n 和 y y y 合并成一个集合。
处理完之后我们只需看有多少个根节点即可。
(2) 代码实现
#include<iostream>
using namespace std;
const int N = 1010;
int pa[2 * N]; // 扩展域并查集, 两种关系,扩展出两个域
int n, m;
void init()
{
for(int i = 1; i <= 2 * n; i++) pa[i] = i;
}
int find(int x)
{
if(pa[x] == x) return x;
return pa[x] = find(pa[x]);
}
// 细节:由于最后我们看有多少个根节点的时候只遍历朋友域
// 因此我们合并的时候要让朋友域中的节点作为父亲节点合并
void uni(int x, int y)
{
// 这段逻辑中 fy 是父亲
int fx = find(x);
int fy = find(y);
pa[fx] = fy;
}
int main()
{
cin >> n >> m;
init();
while(m--)
{
char opt; cin >> opt;
int p, q; cin >> p >> q;
if(opt == 'F') uni(p, q);
else
{
// 注意:让朋友域作为父亲合并
uni(p + n, q);
uni(q + n, p);
}
}
int cnt = 0;
for(int i = 1; i <= n; i++)
{
if(pa[i] == i) cnt++;
}
cout << cnt << endl;
return 0;
}
2. 食物链 ⭐⭐⭐
【题目链接】
[P2024 NOI2001] 食物链 - 洛谷
【题目描述】
动物王国中有三类动物 A , B , C A,B,C A,B,C,这三类动物的食物链构成了有趣的环形。 A A A 吃 B B B, B B B 吃 C C C, C C C 吃 A A A。
现有 N N N 个动物,以 1 ∼ N 1 \sim N 1∼N 编号。每个动物都是 A , B , C A,B,C A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:
- 第一种说法是
1 X Y,表示 X X X 和 Y Y Y 是同类。- 第二种说法是
2 X Y,表示 X X X 吃 Y Y Y。此人对 N N N 个动物,用上述两种说法,一句接一句地说出 K K K 句话,这 K K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X X X 或 Y Y Y 比 N N N 大,就是假话;
- 当前的话表示 X X X 吃 X X X,就是假话。
你的任务是根据给定的 N N N 和 K K K 句话,输出假话的总数。
【输入格式】
第一行两个整数, N , K N,K N,K,表示有 N N N 个动物, K K K 句话。
第二行开始每行一句话。格式见题目描述与样例。
【输出格式】
一行,一个整数,表示假话的总数。
【示例一】
输入
100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5输出
3
【说明/提示】
对于全部数据, 1 ≤ N ≤ 5 × 1 0 4 1\le N\le 5 \times 10^4 1≤N≤5×104, 1 ≤ K ≤ 1 0 5 1\le K \le 10^5 1≤K≤105。
(1) 解题思路
通过题目描述,我们可以得出三类动物之间一共有 3 3 3 种关系:捕食、被捕食、同类。
所以想要维护它们之间的关系可以采用扩展域并查集,扩展出来 3 3 3 个域,需要开辟一个 3 n 3n 3n 的数组。针对某一个动物 x x x( 1 ≤ x ≤ n 1\le x\le n 1≤x≤n),扩展出 3 3 3 个域:同类域 [ 1 , n ] [1, n] [1,n],捕食域 [ n + 1 , 2 n ] [n + 1, 2n] [n+1,2n],被捕食域 [ 2 n + 1 , 3 n ] [2n + 1, 3n] [2n+1,3n]。
如果 x x x 和 y y y 是同类,那么:
- x x x 和 y y y 是同类,合并 x x x 和 y y y;
- x x x 和 y y y 所捕食的物种是同类,合并 x + n x + n x+n 和 y + n y + n y+n;
- 捕食 x x x 和 y y y 的物种是同类,合并 x + 2 n x + 2n x+2n 和 y + 2 n y + 2n y+2n。
如果 x x x 捕食 y y y,那么:
- x x x 捕食 y y y,合并 x x x 和 y + n y + n y+n;
- x x x 捕食的物种和捕食 y y y 的物种是一类,合并 x + n x + n x+n 和 y + 2 n y + 2n y+2n;
- 捕食 x x x 的物种和 y y y 捕食的物种是一类,合并 x + 2 n x + 2n x+2n 和 y + n y + n y+n。
(2) 代码实现
#include<iostream>
using namespace std;
const int N = 5e4 + 10;
int pa[3 * N];
int n, k;
void init()
{
for(int i = 1; i <= 3 * n; i++) pa[i] = i;
}
int find(int x)
{
if(pa[x] == x) return x;
return pa[x] = find(pa[x]);
}
void uni(int x, int y)
{
int fx = find(x);
int fy = find(y);
pa[fx] = fy;
}
bool issame(int x, int y)
{
return find(x) == find(y);
}
int main()
{
scanf("%d%d", &n, &k);
int cnt = 0; // 记录假话个数
init();
while(k--)
{
int r, x, y;
scanf("%d%d%d", &r, &x, &y);
// 如果有一个比 n 大或者 x 吃 x
if((x > n || y > n) || (r == 2 && x == y))
{
cnt++;
continue;
}
// x, y 是同类关系
if(r == 1)
{
// 如果发现 x 如吃 y 或者 x 被 y 吃
bool t = issame(x, y + n) || issame(x, y + 2 * n);
if(t) // 那么就矛盾了,说明是假话
{
cnt++;
continue;
}
// 否则就是真话
uni(x, y);
uni(x + n, y + n);
uni(x + 2 * n, y + 2 * n);
}
// x 吃 y
else
{
// 此时如果发现 x, y 是同类或者 x 被 y 吃
bool t = issame(x, y) || issame(x, y + 2 * n);
if(t) // 那么就矛盾了,说明是假话
{
cnt++;
continue;
}
// 否则就是真话
uni(y + n, x);
uni(x + n, y + 2 * n);
uni(x + 2 * n, y);
}
}
cout << cnt << endl;
return 0;
}
1316

被折叠的 条评论
为什么被折叠?



