一 并查集介绍
并查集是一种维护集合的数据结构,并查集名字“并” “查” “集”分别取自Union Find Set这3个单词。并查集支持下面2个操作
(1)合并:合并2个集合。
(2)查找:判断2个元素是否在一个集合中。
并查集是用数组实现的。
int father[i];
father[i]表示元素i的父亲节点,而父亲节点本身也是这个集合中的元素。father[1]=2表示元素1的父亲节点是元素2,以这种父亲关系来表示元素所属的集合。如果father[i]=i则表示元素i是这个集合的根节点。
对同一个集合来说只存在一个根节点,且将其作为所属集合的标识。
如图所示,1 2 3 4同属于一个集合,根节点是1。5 6同属于一个集合,根节点是5。这是2个不同的集合。
二 并查集的基本操作
1 初始化
一开始,每个元素都是一个独立的集合
for(int i=1;i<=N;i++)
{
father[i]=i;
}
2 查找
由于集合中只存在一个根节点,因此查找操作就是对给定的节点寻求根节点的过程。实现的方式可以是递归和循环。直到找到father[i]=i的点。
递推/循环
int findfather(int x)
{
while(x!=father[x])
{
x=father[x];
}
return x;
}
递归
int findfather(int x)
{
if(x==father[x])
return x;
else
return findfather(father[x]);
}
3 合并
合并指把2个集合合并为1个集合。一般给出2个元素,要求把这2个元素所在的集合合并。
一般,我们需要先判断这2个元素是否属于2个集合,如果不是,就不用合并。
如果不在同一个集合,可以把其中一个集合的根节点的父亲指向另一个集合的根节点。
void Union(int a,int b)
{
int faA=findfather(a);
int faB=findfather(b);
if(faA!=faB)
{
father[faA]=faB;
}
}
在合并的过程中,只对2个不同的集合进行合并,如果2个元素在相同的集合中,就不会进行操作。保证了在集合中一定不会出现环,即并查集产生的每一个集合都是树。
三 路径压缩
上述并查集查找函数没有经过优化,因为在极端情况下效率极低。如果有100000个数字连接成一条链,那么查询就需要100000次。
优化:
因为查找都是查找根节点,那么可以直接令father[i]=根节点即可。即把当前查询节点的路径上的所有节点的父亲都指向根节点。
如何实现:
之前的查找函数,是从给定节点不断获得其父亲节点而最终到达根节点。
转换过程可以分为:
(1)按照查找根节点的写法先获取x的根节点r
(2)重新从x走一遍寻找根节点的路程,把路径上经过的所有节点的父亲全改为根节点r。
循环写法
int findfather(int x)
{
int a=x; //保存初始节点
while(x!=father[x])
{
x=father[x];
}
//现在x是根节点,现在把路径上所有节点的父亲节点都改为根节点
while(a!=father[a])
{
int z=a; //保存当前节点
a=father[a];
father[z]=x;
}
return x; //返回根节点
}
递归写法
int findfather(int x)
{
if(x==father[x])
return x;
else
{
int f=findfather(father[x]);
father[x]=f;
return f;
}
}
四 题目练习
(一) 好朋友
题目描述:
有一个叫做数码宝贝的奇异空间,其中有很多数码宝贝,他们之间可以是好朋友。
A与B是好朋友意味着B与A也是好朋友。
A与C是好朋友,B与C是好朋友,则A与B也是好朋友。
现在给出这些宝贝中所有好朋友的信息。问:这些宝贝可以分为多少组(每组内任意2个宝贝都是好朋友),且任意2组之间的宝贝都不是好朋友。
输入格式
第一行有2个整数n和m(都小于等于100),分别表示数码宝贝的个数和好朋友的组数。接下来有m行,每行2个整数表示一组朋友关系。
输出格式
输出一个整数,表示这些宝贝可以被分为的组数。
样例输入
4 2
1 4
2 3
样例输出
2
思路
每个好朋友组可以看作一个集合,朋友关系可以看作2个节点的边。在输入这些好朋友关系时就可以同时进行并查集的合并操作,在处理完毕后得到一些集合,集合的个数就是组数。
对于集合的个数,由于每个集合都有唯一的一个根节点作为所属集合的标识。因此可以开一个bool数组记录每个节点是否作为集合的根节点。
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=110;
int father[maxn];
bool isRoot[maxn];
int findfather(int x)
{
while(x!=father[x])
{
x=father[x];
}
return x; //返回根节点
}
void Union(int x,int y)
{
int fax=findfather(x);
int fay=findfather(y);
if(fax!=fay)
{
father[fax]=fay;
}
}
int main()
{
int n,m,a,b;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
father[i]=i;
isRoot[i]=false;
}
for(int i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
Union(a,b);
}
for(int i=1;i<=n;i++)
{
isRoot[findfather(i)]=true;
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans+=isRoot[i];
}
printf("%d\n",ans);
return 0;
}
(二)几个岛
岛的个数等于原来岛的个数-新建立的岛与原来的岛建立的桥数(用并查集算出来的桥)+1
#include <iostream>
using namespace std;
int X[4]={0,0,-1,1};
int Y[4]={1,-1,0,0}; //2个数组模拟四个方位
const int maxn=100;
int a[maxn][maxn]={0}; //地图初始化为水
int flags[10000]={0}; //flags[i]表示元素i的父亲节点
int b[10000]={0}; //存储最终岛屿数目
int m,n,k,sum;
int find_father(int x)
{
while(x!=flags[x])
{
x=flags[x];
}
return x;
}
void check(int x,int y)
{
for(int i=0;i<4;i++)
{
int newx=x+X[i];
int newy=y+Y[i];
if(newx<0||newx>=m||newy<0||newy>=n||a[newx][newy]==0)
{
continue;
}
int f_1=find_father(a[x][y]);
int f_2=find_father(a[newx][newy]);
if(f_1!=f_2)
{
sum--;
flags[f_1]=flags[f_2];
}
}
}
int main()
{
while(cin>>m>>n>>k)
{
int x,y;
sum=0;
for(int i=1;i<=k;i++)
{
cin>>x>>y;
if(x<0||x>=m||y<0||y>=n||a[x][y]!=0) //若输入的坐标超出了边界/重复,岛屿个数没有变化,但仍需要输出数值
{
b[i]=sum;
continue;
}
flags[i]=i;
a[x][y]=i; //极其重要
check(x,y);
b[i]=++sum;
}
for(int i=1;i<=k;i++)
{
if(i!=k)
cout<<b[i]<<" ";
else
cout<<b[i]<<endl;
}
}
return 0;
}