一、并查集的概述
1. 概念相关
(1) 并查集的引入
问题
有若干个集合,{a},{b},{c},{d},{e}
设计两个方法
- 判断两个元素是否在同一个集合中 issameset(a, b)
- 将a,b元素所在集合进行合并 union(a, b)
解决方案
给每个集合确定一个代表元素,向上指向某个元素
判断两个元素是否在同一个集合中,就是判断两个元素所在集合的代表元素是否相同
若要合并a,b两个集合的元素,先判断两个元素是否属于一个集合(调用issameset方法),
a,b不属于同一个集合,进行合并操作, 将 b往上指的指针变成a ;找到某个集合的顶(代表元素)把它挂载另外一个元素的顶上
采用往上指的图的方式来表示集合结构
(2) 图示说明
2. 并查集的板子
//并查集结构,及方法
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 100;
int par[100]; //父亲 par[x]=x时,x是所在的树的根
int rank[100]; //树的高度
//初始化n个元素
void init(int n)
{
for(int i=0;i<n;i++)
{
par[i] = i;
rank[i] = 0;
}
}
//查询树的根
int find(int x)
{
if(par[x]==x){
return x;
}else {
return par[x] = find(par[x]); //这种方式,可以将路径进行压缩
}
}
//合并x和y所属的集合
void unite(int x, int y)
{
//找到两个元素的根元素
x = find(x);
y = find(y);
if(x==y) return; //在同一个根上, 属于同一个集合,直接返回
if(rank[x] < rank[y]) //判断哪个元素所在的树比较高
{
par[x] = y; //x的根变为y
}
else
{
par[y] = x; //y的根变成x
if(rank[x]==rank[y]) rank[x]++; //两棵树原来就具有相等的高度,将一棵树挂载到另一棵树上,高度需要加1
}
}
//判断a,b元素是否属于同一集合
bool same(int x, int y)
{
return find(x) == find(y);
}
二、并查集的应用
(一)P1536 村村通题解
1. 题目的算法考查
并查集
2. 程序设计思想
- 两个数组 par:用来保存每个村庄所在树的根节点;H:H[i]表示,以i为根节点的树的高度,在合并时使用,将高度小的数合并到高度大的树上面。
- 将每个村看成一个树的节点,初始化时,每个村子都是一个独立的树,它们的根节点就是它们本身;
- 根据给出的道路关系,将村庄进行连接,怎样说明两个村子连接呢?让它们的根节点变成一样的即可,用union函数实现。
举例:用数组par保存的是每个村子所在树的根节点,在最开始,编号为1的村子,par[1]=1, 编号为2的村子,par[2]=2. 假设题目给出的道路中,1号村子和2号村子有一条路存在,那么就将2号村子所在树挂载到1号树上,让2号村子的根节点变为1,即合并后par[2]=1。此时1号村子和2号村子的根节点都是1,则它们是相同的。
- 最后要统计哪些村子没有相连,那什么情况下所有的村子均相连呢?即,所有村子的根节点相同即可。设一个统计变量ret,用来记录还需要修建几条路,遍历每一个村子,检查它和其他村子的根节点是否相同(注意:假设在查看1号村庄时已经对1号村庄和2号村庄之间的关系确定过了,在查看2号村庄与其他村庄时就不必再管1号村庄和2号村庄的关系),如果不同,ret+1,并用unite函数将两个村庄连接。最终ret统计的值,就是还需要修建的道路的数目。
3. 代码
#include <iostream>
#include <cstring>
//https://www.luogu.com.cn/problem/P1536
using namespace std;
const int maxn=10005;
int n, m; //城镇数目n和道路数目m
int par[maxn]; //用来保存每个城镇所在树的根节点
int H[maxn]; //用来保存树的高度
void init(int n) //初始化
{
for(int i=1;i<=n;i++)
{
par[i] = i;
H[i] = 1;
}
}
/*
a可以到达 b,c 那么 a,b,c三个地方的根节点是相同的
*/
int find(int a) //寻找根节点
{
if(par[a] == a)
return a;
else
return par[a] = find(par[a]);
}
void unite(int a,int b) //对a,b所在集合进行合并
{
a = find(a);
b = find(b);
if(a==b)
return;
if(H[a]>=H[b])
{
par[b] = a;
if(H[a]==H[b]) H[a]++;
}
else
par[a] = b;
}
bool isReach(int a, int b) //判断a,b两个村镇是否可以到达
{
return find(a) == find(b);
}
void solve()
{
//接收道路的关系
int a, b;
//输入道路相连的数据
for(int i=0;i<m;i++)
{
cin >> a >> b;
unite(a,b); //将a和b进行合并
}
//判断还需要几条路才能相连
//遍历每一个村镇
int ret=0;
for(int i=1;i<=n;i++)
{
for(int j=i+1;i<=n;i++)
{
if(!isReach(i,j)) //两个村镇没有相连,则需要一条道路
{
ret++;
unite(i,j);
}
}
}
cout << ret << endl;
}
int main()
{
while(true)
{
cin >> n;
if(n==0)
break;
cin >> m;
memset(par,0,sizeof(par));
memset(H, 0, sizeof(par));
init(n); //初始化
//输入两个数
solve();
}
return 0;
}
(二)P1551亲戚
1. 代码
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 5005;
int par[maxn]; //根节点 par[i]=2 第i个元素的根节点为2
int treeHigh[maxn]; //表示树的高度 treeHigh[i] 表示以i为根节点的树的高度
//初始化 n 个元素
void init(int n)
{
for(int i=1;i<=n;i++)
{
par[i] = i;
treeHigh[i] = 1;
}
}
//查询树根
int find(int x)
{
if(par[x]==x)
return x;
else
{
par[x] = find(par[x]);
return par[x];
}
}
//合并
void unite(int x, int y)
{
//先找到x,y元素的根节点
x = find(x);
y = find(y);
if(x == y) return;
if(treeHigh[x] >= treeHigh[y])
{
par[y] = x; //进行合并,将y的根变成x的根
if(treeHigh[x] == treeHigh[y]) treeHigh[x]++;
}
else
{
par[x] = y; //x的根节点变为y
}
}
bool isSameSet(int x, int y)
{
return find(x) == find(y);
}
int main()
{
int n, m, p;
int a, b;
cin >> n >> m >> p;
init(n); //对这n个人进行初始化
//输入m个关系
for (int i = 0; i < m; i++)
{
cin >> a >> b;
//对a, b进行合并
unite(a, b); //亲戚合并
}
for(int j=0;j<p;j++)
{
cin >> a >> b;
if(isSameSet(a,b))
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}