用处:
1、将两个集合合并
2、询问两个元素是否在同一个集合当中
(这两个操作的时间复杂度都是近乎O(1)的)
基本原理:
每个集合用一棵树来表示,树的编号就是树根的编号。
每个节点存储它的父结点,用p数组来存,p[x]就表示x的父结点。
问题:
1、如何判断树根?
令除了根节点之外,p[x]都不等于x。
2、如何求x的集合编号?
从x一路往上走,走到树根就可。
while(p[x] != x) x = p[x]; // 只要x不是树根我就一直往上走,直到走到树根为止
3、如何合并两个集合编号?
假设px是x的集合编号,py是y的集合编号。合并即令px = y。
板子:
(1)朴素并查集
int p[N]; // 存储每个点的祖宗点
//返回x的祖宗节点
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
//初始化,假定节点编号是1~n
int i;
for(i = 1;i <= n;i ++) p[i] = i;
//合并a和b所在的两个集合
p[find(a)] = find(b);
(2)维护size的并查集
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的意义, 表示祖宗节点所在集合中的点的数量
//返回x的祖宗节点
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
//初始化,假定节点编号是1~n
int i;
for(i = 1;i <= n;i ++)
{
p[i] = i;
size[i] = 1;
}
//合并a和b所在的两个集合
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
(3)维护到祖宗节点距离的并查集
int p[N], d[N];
//p[]存储每个点的祖宗节点,d[x]存储x到p[x]的距离
//返回x的祖宗节点
int find(int x)
{
if(p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
//初始化,假定节点编号是1~n
int i;
for(i = 1;i <= n;i ++)
{
p[i] = i;
d[i] = 0;
}
//合并a和b所在的两个集合
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题, 初始化find(a)的偏移量
题目:
题目1:(合并集合)
题目描述:
一共有 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
。每个结果占一行。
-
数据范围
1≤n,m≤105
输入样例:
4 5 M 1 2 M 3 4 Q 1 2 Q 1 3 Q 3 4
输出样例:
Yes No Yes
AC代码:
#include<cstdio>
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int p[N]; // father数组:存储每个节点的父节点是谁
//初始的时候每个元素都是单独一个集合即每个集合里面只有一个点,这个点的树根就是自己
int find(int x) // 寻找祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = p[find(x)]; // 如果x不是根节点的话,就然让它的父结点等于它的祖宗节点
return p[x]; // 返回父结点
}
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++) p[i] = i; // 初试时把每个节点的p值赋成自己
while(m --)
{
char op[2];
int a,b;
cin >> op >> a >> b;
if(op[0] == 'M') p[find(a)] = find(b);
else
{
if(find(a) == find(b)) cout << "YES" << endl;
else cout << "NO" << endl;
}
}
return 0;
}
题目2:(连通块中点的数量)
题目描述:
给定一个包含 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所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int p[N]; // father数组:存储每个节点的父节点是谁
//初始的时候每个元素都是单独一个集合即每个集合里面只有一个点,这个点的树根就是自己
int number[N]; // 存储集合的元素数量 // 规定:我们只会看根节点的number
int find(int x) // 寻找祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); // 如果x不是根节点的话,就然让它的父结点等于它的祖宗节点
return p[x]; // 返回父结点
}
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int n,m;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++)
{
p[i] = i; // 初试时把每个节点的p值赋成自己
number[i] = 1; // 最开始的时候每个集合只有一个点
}
while(m --)
{
string op;
int a,b;
cin >> op;
if(op == "C")
{
cin >> a >> b;
if(find(a) == find(b)) continue; // a和b已经在同一集合
number[find(b)] += number[find(a)];
p[find(a)] = find(b);
}
else if(op == "Q1")
{
cin >> a >> b;
if(find(a) == find(b)) cout << "Yes" << endl;
else cout << "No" << endl;
}
else
{
cin >> a;
cout << number[find(a)] << endl;
}
}
return 0;
}
题目3:(无线网络)
题目描述:
一个二维平面中放着 n台电脑。电脑编号 1∼n。每台电脑的具体位置已知。
初始时,所有电脑都是关机状态。两台电脑可以直接通信当且仅当两台电脑都处于开机状态,并且两电脑之间距离不超过 d。
电脑之间还可以通过中介电脑实现间接通信。
例如,电脑 A既可以与电脑 B 实现直接通信,也可以与电脑 C 实现直接通信,那么电脑 B 和电脑 C 就可以借助电脑 A
实现间接通信。
现在,要按顺序进行若干个操作,操作共分为两种:
O p
,将电脑 p开机。
S p q
,询问电脑 p和电脑 q之间能否实现通信。-
输入格式
第一行包含两个整数 n,d。
接下来 n行,每行包含两个整数 xi,yi,表示一台电脑的位置坐标。
接下来若干行,每行包含一个操作命令,格式入题面所述。
-
输出格式
对于每个询问指令,输出一行答案,如果可以实现通信,则输出
SUCCESS
,否则输出FAIL
。数据范围
1 ≤ n ≤ 1001
,
0 ≤ d ≤20000,
0 ≤ xi,yi ≤ 10000,
1 ≤ p , q ≤ , q ≠ p
最多包含 3×10^5个操作指令。 -
输入样例:
4 1 0 1 0 2 0 3 0 4 O 1 O 2 O 4 S 1 4 O 3 S 1 4
输出样例:
FAIL SUCCESS
AC代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> coo;
const int N = 1e4 + 10;
coo a[N];
int p[N],book[N]; // book数组用来标记电脑状态,表示关闭,表示打开
//寻找x的祖宗节点
int find(int x)
{
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
// 计算距离
double dis(coo a, coo b)
{
return (double)sqrt((a.first - b.first) * (a.first - b.first) + (a.second - b.second) * (a.second - b.second));
}
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int n,d;
cin >> n >> d;
for(int i = 1 ; i <= n ; i ++)
{
int x,y;
cin >> x >> y;
a[i] = {x,y};
}
for(int i = 1 ; i <= n ; i ++) p[i] = i; // 初始化
string s;
while( cin >> s)
{
if(s == "O")
{
int id;
cin >> id;
book[id] = 1; // 电脑打开
for(int i = 1 ; i <= n ; i ++)
{
if(i == id) continue; // 一台你说怎么连
else if(book[i] == 1 && dis(a[id],a[i]) <= d) // 电脑是打开的状态并且他们之间的距离小于d
{
p[find(id)] = find(i); // 两台电脑联系起来
}
}
}
else
{
int u,v;
cin >> u >> v;
if(find(u) != find(v)) cout << "FAIL" << endl; // u和v是不是一个祖宗节点
else cout << "SUCCESS" << endl;
}
}
return 0;
}
题目 4:(可疑人员)
题目描述:
某学校共有 n 个学生,编号 0∼n−1。
该学校共有 m个社团。
每个学生可以加入多个社团,也可以不参加任何社团。
该学校所在的地区爆发了非典型肺炎。经查实,0号学生为病毒携带的可疑人员,需要进行隔离观察。为了防止疫情传播,学校还规定,一旦某社团出现病毒携带的可疑人员,则该社团的全体成员将全部被划分为病毒携带的可疑人员。请你计算,该学校最终共有多少名学生被判定为病毒携带的可疑人员。
输入格式
输入包含多组测试数据。
每组数据第一行包含两个整数 n,m。
接下来 m行,每行描述一个社团的人员,首先包含一个整数 k,表示该社团的人数,然后包含 k
个整数,表示每个社员的编号。
当输入 n=0,m=0时,表示输入结束。
输出格式
每组数据输出一行结果,表示被判定为病毒携带的可疑人员的学生数量。
数据范围
输入最多包含 10组数据。
1≤n≤30000,
0≤m≤500,
1≤k≤n。
输入样例:
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
输出样例:
4
1
1
AC代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
#include<map>
#define inf 0x3f3f3f3f
using namespace std;
const int N = 30010;
int p[N];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
int n,m; // 学校总人数,社团的数量
while (~scanf("%d %d",&n,&m) && (m != 0 || n != 0))
{
for (int i = 0; i < n; i++) p[i] = i; // 初始化并查集
while (m--)
{
int k;
scanf("%d",&k); //社团内部的元素数量
int x;
scanf("%d",&x); // 输入该社团的第一个成员
for (int i = 2; i <= k; i++)
{
int y;
scanf("%d",&y); // 依次输入社团后面的成员
p[find(x)] = find(y); // 将该社团的后面点全部与第一个点合并
}
}
int cnt = 0;
for (int i = 0; i < n; i++) // 遍历所有学生
{
if(find(i) == p[0]) cnt ++;
}
printf("%d\n",cnt);
}
return 0;
}