并查集(Disjointed set)
前言
并查集(Disjointed Set)是一种非常精巧而实用的数据结构,用于处理不相交集合的合并、查找
经典应用:
- 连通性判断
- 最小生成树Kruskal算法
- 最近公共祖先
应用背景1 “帮派”
- 有n个人,他们属于不同的帮派;
- 如果1号、2号是朋友,1、3号也是朋友,那么他们属于一个帮派;
- 问有多少个帮派,每人属于哪个帮派?
- 用并查集能简洁的表示这个关系。
应用背景2 “饭桌”
- 有n个人在一起吃法,有些人互相认识
- 认识的人想坐在一起而不想跟陌生人坐
- 例如:A认识B,B认识C,那么A、B、C会坐到一张桌子上
- 给出认识的人,问需要多少张桌子?
并查集的基本操作
初始化
定义int s[ ]是以结点i为元素的并查集
初始化:令s[i] = i
初始的时候每个人都是一个独立的帮派
//初始化
void init_set(){
for(int i = 1;i<N;i++)
s[i] = i;
}
合并
例1:加入第一个朋友关系(1,2)
- 在并查集s中,把结点1合并到结点二,也就是把结点1的集1改为结点2的集2
例2:加入第二个朋友关系(1,3)
- 查找结点1的集,是2,递归查找元素2的集是2
- 把元素2的集2合并到结点3的集3.此时结点1、2、3都是属于一个集。
例3:加入第三个朋友关系(2,4)
- 同上
-
//合并
void union_set(int x,int y){
x = find_set(x);
y = find_set(y);
if(x!=y)s[x] = s[y];
}
查找
- 查找元素的集:是一个递归,直到元素的值和它的集相等,就找到了根结点的集
//查找(递归)
int find_set(int x){
return x == s[x]?x:find_set(s[x]);
}
有多少个集?
如果s[i] = i ,这是一个根结点,是它所在的集的代表(帮主)
- 统计根结点的数量,就是集的数量。
基本代码的复杂度 - 查找find_set()、合并union_set()的搜索深度是树的长度,复杂度为O(n).
优化
合并的优化(不常用)
- 合并x和y时,先搜到他们的根结点
- 合并这两个根结点:把一个根结点的集改成另一个根结点
- 这两个根结点高度不同时,把高度较小的集合并到较大的集上,能减少数的高度。
查询的优化:路径压缩
- 查询find_set():沿着搜索路径找到根结点,这条路径可能很长。
- 优化:沿路径返回时顺便把所属的集改成根结点
- 下次再搜,复杂度为O(1)
int find_set(int x){//没有路径压缩
return x ==s[x]?x:find_set(s[x]);
}
int find_set(int x){有路径压缩
if(x!=s[x]) s[x] = find_set(s[x]);
return s[x];
}
例题
蓝桥幼儿园
- 题目描述:蓝桥幼儿园的学生天真无邪,对她们来说,朋友的朋友就是自己的朋友。
- 小明是蓝桥幼儿园的老师,这天他决定为学生举办一个交友活动,活动规则:用红绳连接两名学生,被连接的两个学生将成为朋友。
- 请你帮忙写程序判断两个学生是否为朋友(默认自己和自己也是朋友)
- 小朋友数:n<800000
#include<iostream>
#define N 800005
int s[N];
using namespace std;
void init_set(int n) {
for (int i = 0; i < n; i++)
s[i] = i;
}
int find_set(int x) {
if (x != s[x]) s[x] = find_set(s[x]);
return s[x];
}
void merge_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x != y) s[x] = s[y];
}
int main() {
int n, m;
cin >> n >> m;
init_set(n);
int x, y;
for (int i = 0; i < m; i++) {
cin >> x >> y;
merge_set(x, y);
}
cin >> x >> y;
if (find_set(x) == find_set(y)) cout << "YES";
return 0;
}
合根植物
- 题目描述:一种植物园,被分成m*n个小格子(东西方向m行,南北方向n行,每个格子里种了一株和根植物)
植物的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物和为一体 - 如果我告诉你那些小格出现了连根现象,你能说出共有多少株合根植物吗?
并查集的简单应用:判断连通性
并查集的方法:
- (1):初始化:
- 把5个数初始化为5个集合,开始时他们属于不同的集合,说明这5个数字不连通
- (2):连通(合并)
- 如果两个数字相连,把这两个数字放到一个集合中;
- 如果在一个集合中他们就连通。
- (3):连通查询
- 查询5个数是否是属于一个集合,如果是就是连通的。
- 题目:有12张连在一起的12生肖的邮票从中剪下5张,要求必须是连着的,(仅连一个角不算相连)
- 比如图中,粉红色所示的部分就是合格的剪取
#include<iostream>
using namespace std;
int star[] = { 1,2,3,4,5,6,7,8,9,10.11,12 };
int num = 0;
int b[] = { -1,1,-5,5 };
bool check() {//检查star[0]~star[4]这5个数是否连通
int set[5] = {0,1,2,3,4 };//用并查集判断5个数是否连通,初始值有5个集合,编号0~4
for (int i = 0; i <= 4; i++) {//第i个数和第j个数是否相邻
for (int j = 0; j <= 4; j++) {
for (int k = 0; k <= 3; k++) {//k是上下左右四个方向
if (star[i] + b[k] == star[j]) {
int temp = set[j];
set[j] = set[i];
for (int s = 0; s <= 4; s++) {
if (set[s] == temp)
set[s] = set[i];
}
}
}
}
}
for (int i = 1; i <= 4; i++) {
if (set[0] != set[i])return false;
return true;
}
}