目录
并查集用法介绍:
- 将两个集合合并
- 询问两个元素是否在一个集合当中
基本原理 :
每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
问题:
- 判断树根:if(p[x] == x)(即除了根节点之外,p[x]都不等于x)
- 求x的集合编号: while(p[x] != x) x = p[x];(只要不是树根就一直往上走,最后x就是它的集合编号/树根编号)
- 合并两个集合: p[x]是x的集合编号,p[y]是y的集合编号。 p[x]=y(把一个集合的根节点作为另一个集合的儿子)
路径压缩优化时间复杂度,查找集合编号时,把在路径上的儿子直接指向根节点
模板
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int 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
for (int 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
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
作者:yxc
来源:AcWing
//合并集合
void merge_set(int i, int j){
a = find(i);
b = find(j);
if(a != b) p[a] = b;
}
例题
给定一个包含 n 个点(编号为 1∼n1)的无向图,初始时图中没有边。
现在要进行 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≤1051
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
//father数组,存储每个元素的父节点是谁
int p[N];
//寻找根节点并返回
//返回x的祖宗节点+路径压缩
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);//如果p[x]不是祖宗节点,就让p[x]等于祖宗节点
return p[x];
}
int main()
{
//存储集合里有几个节点
int size[N];
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i ++)
{
p[i] = i;//初始化,每个节点的根节点就是自己
size[i] = 1;
}
while(m--)
{
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);//scanf读字符的时候会读入空格,读字符串的时候会自动忽略空格
//让a的祖宗节点的父亲等于b的祖宗节点
if(op[0] == 'C')
{
scanf("%d%d", &a, &b);
//判断a,b是否在同一个集合中
if(find(a) == find(b)) continue;
size[find(b)] += size[find(a)];
//a的根节点的父节点是b的根节点,即把两个不同的集合合并,把集合A作为集合B的儿子
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", size[find(a)]);
}
}
return 0;
}
链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网
题号:NC15808
时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 32768K,其他语言65536K
64bit IO Format: %lld
题目描述
平面上有若干个点,从每个点出发,你可以往东南西北任意方向走,直到碰到另一个点,然后才可以改变方向。
请问至少需要加多少个点,使得点对之间互相可以到达。
输入描述:
第一行一个整数n表示点数( 1 <= n <= 100)。 第二行n行,每行两个整数xi, yi表示坐标( 1 <= xi, yi <= 1000)。 y轴正方向为北,x轴正方形为东。
输出描述:
输出一个整数表示最少需要加的点的数目。
示例1
输入
复制2 2 1 1 2
2 2 1 1 2
输出
复制1
1
示例2
输入
复制2 2 1 4 1
2 2 1 4 1
输出
复制0
0
#include<bits/stdc++.h>
#include<iostream>
using namespace std;
const int N = 1010;
int n;
int xy[N][2];
int p[N];
int a, b;
//寻找父节点方法
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
//合并方法
void meger(int i, int j){
a = find(i);
b = find(j);
if(a != b) p[a] = b;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d%d", &xy[i][0], &xy[i][1]);
p[i]=i;//父节点初始化
}
//遍历每个点寻找x或y相同的点,并合并形成连通块
for(int i = 1; i <= n; i++){
for(int j = i + 1; j <= n; j++){
if(xy[i][0] == xy[j][0] || xy[i][1] ==xy[j][1])//连通条件
{
meger(i, j);//合并成连通块
}
}
}
int ans = 0;
//记录有多少个连通块
for(int i = 1; i <= n; i++){
if(p[i] == i){
++ans;
}
}
printf("%d\n", ans - 1);
return 0;
}