一、基本知识
1. 作用
(1)将两个集合合并
(2)询问两个元素是否在一个集合当中
2.基本原理
以树的形式维护每个集合, 每个集合的根节点编号就是该集合的编号。
每个节点都存储他的父节点。 p[x] 表示 x 的父节点
3.三个问题
- 如何判断树根:
if(p[x] == x)
- 如何求 x 的 集合编号
while(p[x] != x) x = p[x]
- 如何合并两个集合
p[x] 是 x 集合编号, p[y] 是 y 集合编号
p[x[ = y
即可。
4.一个优化
路径压缩
找到根节点后,直接把路径上所有点直接指向根节点
二、合并集合
模板题
题目链接
题目描述
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。
//一共n个数, 编号1-n, 最开始每个数各自在一个集合中,
//进行m个操作 "Mab"将编号为a,b的两个数的集合合并, "Qab"询问看是否在同一集合
#include <iostream>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N]; //存储每个元素的父节点是谁
int find(int x) //返回x所在集合的编号(祖宗结点) 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //x不是根节点, 让父节点等于祖宗节点
return p[x];
}
int main(void){
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) p[i] = i; //初始化
while(m--){
char op[2]; //读入单个字符,%c 容易读入一些莫名其妙的字符
int a, b;
scanf("%s%d%d", op, &a, &b);
if(op[0] == 'M') p[find(a)] = find(b);
else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
三、连通块中点的数量
题目链接
题目描述
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。
现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
输出格式
对于每个询问指令”Q1 a b”,如果a和b在同一个连通块中,则输出“Yes”,否则输出“No”。
对于每个询问指令“Q2 a”,输出一个整数表示点a所在连通块中点的数量
每个结果占一行。
分析
可以再设一个数组,只保证根节点的size有意义即可
每次合并两集合时,更新根节点的值即可。
代码
//一共n个数, 编号1-n, 最开始每个数各自在一个集合中,
//进行m个操作 "C a b"将编号为a,b的两个数的集合合并, "Q1ab"询问看是否在同一集合
//Q2a 询问a所在连通块中点数量
#include <iostream>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N], size1[N]; //存储每个元素的父节点是谁
//只保证根节点的size是有意义的
int find(int x) //返回x所在集合的编号(祖宗结点) 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //x不是根节点, 让父节点等于祖宗节点
return p[x];
}
int main(void){
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) p[i] = i, size1[i] = 1; //初始化
while(m--){
char op[2]; //读入单个字符, 容易读入一些莫名其妙的字符
int a, b;
scanf("%s", op);
if(op[0] == 'C'){
scanf("%d%d", &a, &b);
if(find(a) == find(b)) continue;//当a,b已经在同一集合里,直接跳过即可
// p[find(a)] = find(b);find函数会用到p[]数组,所以不能先改变
size1[find(b)] += size1[find(a)];
p[find(a)] = find(b);
}
else if(op[1] == '1'){
scanf("%d%d", &a, &b);
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
else{
scanf("%d", &a);
printf("%d\n", size1[find(a)]);
}
}
return 0;
}
四、食物链
题目链接
题目描述:
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。
A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。
每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是”1 X Y”,表示X和Y是同类。
第二种说法是”2 X Y”,表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N和K句话,输出假话的总数。
输入格式
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000,
0≤K≤100000
1.分析
- 同类、被吃等情况,我们都可以把他们放到一个集合里, 集合里的元素,我们可以判断,他们都是有关系的。比如: A 吃 B, B, C同类,我们可以知道,A和C一定有关系
- 如何确定两元素之间的关系?我们可以 记录元素与根节点之间的关系来确定
- 用每个点到根节点的距离来确定关系
A到根节点的距离是1,表示可以吃根节点,%3 == 1
B到根节点的距离是二,到A的距离是1,可以吃A, 表示他是被根节点吃的(由题意可知, 围成的是个环)%3 == 2
C到根节点的距离是3,表示他跟根节点是同类。%3 == 0
我们可以用%3来判断
余1的点可以吃根节点,
余2的点可以吃余1的点,被余0的点吃
余0 与根节点同类。
距离: x吃y, 表示y到x的距离是1
4. 当两者x, y不属于同一类,不在同一个集合里,
我们把y根节点 p[x]接在 p[y]上, 要保证如何记录p[x] d到 p[y]的距离?
(d[x] + ? - d[y] ) % 3 == 0
? = d[y] - d[x]
- 当x, y不属于同一类, x吃y的关系
操作同上,
(d[x] + ? - d[y] - 1 ) % 3 == 0;
? = d[y] + 1 - d[x]
- //不用初始化k[],定义在全局,初始便是0
2.代码
/*
该点到父节点的距离:d[x], d[p[x]] 就是父节点到根节点的距离,
相加即为该点到根节点的距离
*/
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 5e4;
int n, m;
int p[N], d[N];
int find(int x){
if(x != p[x]){// 如果x不是树根
int t = find(p[x]);
//在调用find函数时,会把父节点到根节点的距离返回,此时t存储的是根节点的值
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
int main(void){
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
int res = 0;
while(m --){
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
int px = find(x);
int py = find(y);
if(x > n || y > n) res++;
else if(t == 1){
if(px == py && (d[x] - d[y]) % 3){//如果两者已经在一棵树上了
res ++; //两者不是同一类
}
else if(px != py){//没有在同一个集合里,我们要合并
p[px] = py;//把x接向y的那颗树
d[px] += d[y] - d[x]; //两根之间距离
}
}
else{//判断吃与被吃关系
if(px == py && (d[x] - d[y] - 1) % 3) res ++;
else if(px != py){
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
cout << res << endl;
return 0;
}
五、小希的迷宫
题目描述
上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了房间A和B,那么既可以通过它从房间A走到房间B,也可以通过它从房间B走到房间A,为了提高难度,小希希望任意两个房间有且仅有一条路径可以相通(除非走了回头路)。小希现在把她的设计图给你,让你帮忙判断她的设计图是否符合她的设计思路。比如下面的例子,前两个是符合条件的,但是最后一个却有两种方法从5到达8。
输入:
输入包含多组数据,每组数据是一个以0 0结尾的整数对列表,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。每两组数据之间有一个空行。
整个文件以两个-1结尾。
输出:
对于输入的每一组数据,输出仅包括一行。如果该迷宫符合小希的思路,那么输出"Yes",否则输出"No"。
分析
- 不能产生环
- 只能有一个图,如 1 2 3 4,是两个子图, 不成立
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
int p[N], vis[N]; //存储父节点, 标记数组vis, 标记顶点是否被使用过
int a, b;
int find(int x){//查找根节点
int r = x; //这样不会改变x的值
while(p[r] != r){ //毕竟递归需要时间
r = p[r];
}
return r;
}
int main(void){
while(1){
scanf("%d %d", &a, &b);
if(a == -1 && b == -1)return 0;
if(a == 0 && b == 0){
printf("Yes\n");
continue;
}
for(int i = 1; i <= 1e5; i++){ //每组数据都这样处理,初始化的意味
p[i] = i;
vis[i] = 1;
}
vis[a] = 0;
vis[b] = 0;
if(a != b) p[find(a)] = find(b); //连接
int Min = min(a,b);
int Max = max(a, b);
int mark = 1;//标记是否存在环路
while(1){
scanf("%d %d", &a, &b);
if(a == 0 && b == 0) break;
Max = max(Max, a);
Max = max(Max, b);//便于之后遍历
Min = min(Min, a);
Min = min(Min, b);
vis[a] = 0;
vis[b] = 0;
if(find(a) != find(b)) p[find(a)] = find(b);
else if(a != b) mark = 0;//成环了
}
int cnt = 0;
// printf("%d %d\n", Max, Min);
for(int i = Min; i <= Max; i++){
if(vis[i] == 0 && p[i] == i) cnt ++;
//统计有几个子图
}
if(mark == 1 && cnt == 1) printf("Yes\n");
else printf("No\n");
}
}