一、引入
并查集的功能如下:
如果a和b是舍友,b和c是舍友,则我们认定a和c也是舍友(当然在离散数学中这是错误的,为了引入我们假定自己和自己也是舍友)。也就是说有n个人,给出m个元组(a,b)表示a和b是舍友,现在有许多查询:请你判断任意两个人之间是否有舍友关系。
复杂度来讲,建立和查询加起来大概是O(n)再加上一个常数,效率也比较高。
并查集还可以解决以下问题:
问:如何判断一个简单无向图是否为连通图(图一般会以二元组序列形式给出)
- dfs/bfs直接搜索(空间复杂度为O(n*n) (邻接矩阵存图)、O(n)(邻接表存图,但是vector会额外占用大量时间),时间复杂度为O(n),因为如果联通,每个点都会被搜索到有且仅有一遍)
- 并查集
- ps:并查集空间复杂度只有O(n),代码更加简洁,且查询越多,平均时间复杂度越低。查询极多的时候可以认为查询的复杂度为O(1)。
二、直接讲最优的方法
并查集+优化路径压缩
以上问题可看做分组问题,或者连通图问题
假设有n个人编号1~n,则
1.建立并查集(初始化):
我们以n个人建树,第i个树现在只有i这个root节点。设pre[x]表示x的父亲节点是谁,初始pre[x]=x (假设root节点的父亲还是root,这根linux文件系统有点像)
int const n=1000+1;//总人数
int pre[maxn];//树根
void build{
for(int i=1;i<=n;i++){
pre[i]=i;
}
}
2.建立关系
给出m个关系a,b,表示a和b属于同一组,也就是将两个人归并到一个组的操作,就是把b所在的树挂到a的树上,为了减小树的高度和方便,我们直接把a所在的树挂到b的根上,使a和b同在一棵树上。
当然,如果a和b本来就同在一棵树上,那就不需要合并这个操作了。而判断是否在同一棵树上的依据是是否有同一个根。
我们设find(x)表示x所在树的根节点,那么合并操作代码如下:
void combine(int a,int b){
if(find(a)==find(b))//是否在同一棵树上
return;
pre[find(a)]=find(b);
}
3.查找root
查找root就是find函数的功能,而find(x)函数的实现,则是顺着pre[x]函数一直回溯,直到pre[x]==x就找到root了,(根节点的父亲还是自己)。
回溯次数跟树的高度有关,为了优化,还需要采用优化路径压缩,减小树的高度。也就是在回溯的过程中,不断把当前节点重新挂载到上一层祖先上,只要保证他们的root不变,其他的父亲关系都不影响大局。用代码就是pre[x]=find(pre[x]);(令x的父亲等于爷爷,这就降低了树的高度而不改变共同的root祖先)
int find(int x){
if(pre[x]==x)//如果已经是根节点
return x;
return pre[x]=find(pre[x]);
}
4.判断两者是否在同一组内
判断两者在同一组内的依据就是判断两者是否拥有同一个祖先(之前已经说过了)
bool query(int a,int b){
return find(a)==find(b);
}
完整版并查集代码:(我没测试过,不过写了太多遍了应该不会错)
#include<bits/stdc++.h>
using namespace std;
int const maxn=1000+1;//总人数
int pre[maxn];//树根
void build(int n){
for(int i=1;i<=n;i++){
pre[i]=i;
}
}
int find(int x){
if(pre[x]==x)//如果已经是根节点
return x;
return pre[x]=find(pre[x]);
}
void combine(int a,int b){
if(find(a)==find(b))
return;
pre[find(a)]=find(b);
}
bool query(int a,int b){
return find(a)==find(b);
}
int main(){
int a,b;
int n,m,q;
cin>>n>>m;//输入人数 和关系个数
build(n);
for(int i=1;i<=m;i++){
cin>>a>>b;//a和b是同学
combine(a,b);
}
cin>>q; //输入查询次数
for(int i=1;i<=q;i++){
cin>>a>>b;
if(query(a,b)){
printf("%d和%d是同学\n",a,b);
}
else
printf("%d和%d不是同学\n",a,b);
}
}