目录
一、并查集定义
并查集是一种维护集合的数据结构,用于处理一些不相交集合的合并及查询问题,在数据结构中的作用就是判断图中两点的连通性,是一种空间换时间的算法,比搜索来的要更快。构建完成的并查集从其数据结构上来说是树。
1.并查集支持下面两个操作:
(1)合并:合并两个集合
(2)查找:判断两个元素是否在一个集合
2.并查集的实现:并查集通过数组来实现,在数组中散列某结点的父亲节点,形成一个记录父亲结点的静态链表,通过判断父系关系来表示元素所属的集,类似反向遍历的树。对同一个集合来说只存在一个根结点,将其作为所属集合的标识。
二、并查集的基本操作
1.初始化
一开始,每个元素都是独立的一个集合,因此需要令所有father[i]等于i:
2.查找
查找操作就是对给定的结点寻找其根结点的过程,如果两个元素的根结点相同,说明两个元素在同一个集合中。
3.合并
合并是指把两个集合合并成一个集合,合并的过程一般是把其中一个集合的根结点的父亲指向另一个集合的跟结点。思路如下:
(1)对于给定的两个元素a、b,判断它们是否属于同一集合。可以调用查找函数,对这两个元素a、b分别查找根结点,然后再判断其根结点是否相同。
(2)合并两个集合:在(1)中己经获得了两个元素的根结点faA与faB,因此只需要把其中一个的父亲结点指向另一个结点。例如可以令father[faA]=faB,当然反过来令father[faB]=faA也是可以的,两者没有区别。
三、路径压缩
回忆合并过程,有两个方法,即father[faA]=faB和father[faB]=faA,这两个方法的选择会直接导致最后生成的并查集在形态上大不相同,在极端情况下甚至可能退化成一条链,这样执行查询根结点的算法复杂度会很高。所以书中给出了一种压缩办法,根据并查集的查询操作:寻找某个结点的根结点,可以将某个树中的所有元素的父亲结点都指向根结点,这样查询的复杂度就降到了O(1)
实现思路可以在合并查询的过程中,将子结点到根结点的查询路径上的所有元素的父节点都更改为根结点,具体实现分为两步:
(1)按原先的写法获得x的根结点r;
(2)重新从x开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲全部改为根结点r;
也可以在合并集合时设置父节点都为序号小的那一个
四、题目
1.排座位
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 105;
struct customer{
int fr[N];
int en[N];
};
int fa[N];
int find(int p){
if(fa[p] == p){
return fa[p];
}
else return fa[p] = find(fa[p]);
}
void add(int a,int b){
int x = find(a);
int y = find(b);
if(x < y)
fa[y] = x;
else if(y < x)
fa[x] = y;
}
int main(){
int n,m,k;
cin>>n>>m>>k;
struct customer rela[N];
for(int i = 1;i < n + 1;i++){
fa[i] = i;
memset(rela[i].fr,0,sizeof rela[i].fr);
memset(rela[i].en,0,sizeof rela[i].en);
}
int a,b,re;
for(int i = 0;i < m;i++){
cin>>a>>b>>re;
if(re == 1){
add(a,b);
rela[a].fr[b] = 1;
rela[b].fr[a] = 1;
}
else{
rela[a].en[b] = 1;
rela[b].en[a] = 1;
}
}
int p,q;
for(int i = 0;i < k;i++){
cin>>p>>q;
if(rela[p].fr[q] == 1){
cout<<"No problem"<<endl;
}
else if(rela[p].en[q] != 1){
cout<<"OK"<<endl;
}
else if(find(p) == find(q)){
cout<<"OK but..."<<endl;
}
else{
cout<<"No way"<<endl;
}
}
return 0;
}
2.合根植物
样例输入
5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17样例输出
5
样例说明:其合根情况参考下图(注意:6 也是一个连通子集)
代码
#include<iostream>
using namespace std;
const int MAX=1000010; //数组的最大长度(即图中点个数的最大值)
int m,n; //当前图的长宽规格
int pre[MAX]; //用于存放每个点的根节点
void init(int n) //初始化函数
{
for(int i=1;i<=n;i++)
pre[i]=i;
}
int find_pre(int key) //寻找根节点函数
{
if(pre[key]==key) return key;
return pre[key]=find_pre(pre[key]);
}
void unite(int x,int y) //联合函数
{
int rootx=find_pre(x);
int rooty=find_pre(y);
if(rootx!=rooty) pre[rootx]=rooty;
}
int main()
{
int x,y,line;
cin>>m>>n>>line;
init(m*n);
for(int i=0;i<line;i++)
{
cin>>x>>y;
unite(x,y);
}
int ans=0,a[MAX]={0};
for(int i=1;i<=m*n;i++)
a[find_pre(i)]=1;
for(int i=1;i<=m*n;i++)
if(a[i]) ans++;
cout<<ans<<endl;
return 0;
}
3. 国王的烦恼
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10010;
const int M=100010;
struct Bridge //代表桥
{
int x,y; //表示桥连接的两个地方
int day; //表示这个桥的可用时限(天数)
Bridge(){ }
Bridge(int a,int b,int c):x(a),y(b),day(c){ }
};
Bridge bridge[M]; //用于存储所有的桥
int pre[N]; //用于存储每个小岛的“上级”
void init(int n)
{
for(int i=1;i<=n;i++)
pre[i]=i;
}
int find_pre(int n)
{
if(pre[n]==n) return n;
else return pre[n]=find_pre(pre[n]);
}
bool unite(int x,int y)
{
int rootx=find_pre(x);
int rooty=find_pre(y);
if(rootx!=rooty){
pre[rootx]=rooty;
return true;
}
else return false;
}
bool cmp(Bridge a,Bridge b)
{ return a.day>b.day; }
int main()
{
int n,m,a,b,t;
cin>>n>>m;
init(n);
for(int i=1;i<=m;i++){
cin>>a>>b>>t;
bridge[i]=Bridge(a,b,t);
}
sort(bridge+1,bridge+1+m,cmp);
int ans=0,lastDay=0; //lastDay用于探测一次某个桥的生命时限
for(int i=1;i<=m;i++)
{
bool flag=unite(bridge[i].x,bridge[i].y); //如果为真表示当前这两个岛未联通
if(flag && bridge[i].day!=lastDay) //未连通,且此桥的天数是第一次出现,那么就增加了抗议的天数
{
ans++;
lastDay=bridge[i].day;
}
}
cout<<ans<<endl;
return 0;
}
【蓝桥杯】 历届试题 国王的烦恼(并查集)_c国由n个小岛组成,为了方便小岛之间联络,c国在小岛间建立了m座大桥,每座大桥连接-CSDN博客