并查集的使用情况:
并查集的应用主要是用在多个集合中,取到各个集合之间的关系或者是单个集合之内的关系;
并查集的原理:
1,每个元素的初始父亲都是自己,需要用到的数组也是pre回溯数组,既找到自己的上一个连接元素是谁
2,每个元素对应的父亲的最后一位,就是这个集合的根,根对应的父亲就是他自己
3,多个元素之间,连接的方向其实无所谓,1->3,3->3自己,需要将2加入到集合中,此时3->2和2->3都可以,但总归上,会让各个集合的根相连,2这个集合中只有他本身,所以他就是自己的集合的根,所以2不能与1相连,只能与1,3集合中的根3相连接,至于方向,会使集合中的根不同,一般是让多元素的集合的根连向下一个单独的元素;
并查集的优化:
1,路径压缩存储(time约等于O(1)):
让每个集合中所有元素都在递归中将自己的父亲指向根节点,既所有元素对应的父亲都是该集合的根
注意:
路径压缩主要用于题目有需要的要求时使用,因为会打破原有的结构,但可以大大提高算法的效率;
代码与关建行解释:
#include<bits/stdc++.h>
using namespace std;
const int N=10000;
int pre[N];//用来表示第x位的父亲是谁
int root (int x)
{
return pre[x]=x==pre[x]?x:root (pre[x]);//将每一位元素的父亲都变为根节点
}
int merge(int x,int y)
{
x=root(x),y=root(y);
if(x==y)//说明在同一个集合内
return 0;
pre[x]=y;//写pre[y]=x也可以,目的只是将二者的根相连,形成一个集合
}
void solve()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
pre[i]=i;
while(m--)
{
int op,x,y;
cin>>op>>x>>y;
if(op==1)
{
merge(x,y);
}
else{
cout<<(root(x)==root(y)?'Y':'N')<<endl;
}
}
}
int main()
{
solve();
return 0;
}
其中,root的环节可以采用循环的方式来寻找根节点,因为当数据过大时,递归的方式可能爆栈,所以采用循环的方式就可以避免数据过大导致错误:
int root (int x)
{
int res = x;
while(pre[res]!=res)
res=pre[res];//寻找该集合的根节点res
while(pre[x]!=x)
{
int y=x;
x=pre[x];//x用来记住自己的父亲,用于下一次遍历
pre[y]=res;//将该位的父亲更变为根节点(相对于结果而言)
}
return res;//当循环到最后的节点的时候,输出他自己(所以这里写x也是一样的结果)
}
2,按秩合并:
对于多个集合之间合并为一个大的集合,相当于森林合并成一棵树,为了追求各个集合的元素被根所访问的时间平均最短,一般会让元素少的集合的根连向元素多的集合的根,尽量让整个树变得"又矮又胖";
注意:面对数据量小的题目,与路径压缩的时间复杂度近似,但数据量大差距便会显现;
#include<bits/stdc++.h>
using namespace std;
const int N=10000;
int pre[N],rnk[N];//rnk用来存储该集合的秩,既有多少个元素存在
int root (int x)
{
while(pre[x]^x)//异或符号"^"相当于"!="既二者不相等
x=pre[x];
return x;//根节点
}
int merge(int x,int y)
{
x=root(x),y=root(y);
if(x==y)//说明在同一个集合内
return 0;
if(rnk[x]>rnk[y])
swap(x,y);//用来保证rnk[x]<==rnk[y]始终成立,便于之后的赋值计算
pre[x]=y;//将秩小的x(所在的)集合的根连向y
if(rnk[x]==rnk[y])
rnk[y]++;//当二者的秩相等时,更新其中一位的数据,默认是y
}
void solve()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
pre[i]=i;
while(m--)
{
int op,x,y;
cin>>op>>x>>y;
if(op==1)
{
merge(x,y);
}
else{
cout<<(root(x)==root(y)?'Y':'N')<<endl;
}
}
}
int main()
{
solve();
return 0;
}