持续更新中😬 加个关注,后续上新不错过~
并查集
1. 并查集是什么
并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地进行如下操作。不过需要注意:并查集虽然可以进行合并操作,但是却无法进行分割操作。
- 查询元素a和元素b是否属于同一组
- 合并元素a和元素b所在的组
2. 并查集的结构
并查集也是使用树形结构来实现的。不过,不是二叉树。
每个元素对应一个节点,每个组对应一棵树。在并查集中,哪个节点是哪个节点的父亲以及树的形状等信息无须多加关注,整体组成一个树形结构才是重要的。
(1)初始化
我们准备n个节点来表示n个元素。最开始时没有边。
(2)合并
像下图一样,从一个组的根向另一个组的根连边,这样两棵树就变成了一棵树,也就把两个组合并为一个组了。
(3)查询
为了查询两个节点是否属于同一组,我们需要沿着树向上走,来查询包含这个元素的树的根是谁。如果两个节点走到了同一个根,那么就可以知道它们属于同一组。
在下图中,元素2和元素5都走到了元素1,因为它们属于同一组。另一方面,由于元素7走到的是元素6,因此同元素2或元素5属于不同组。
3. 并查集实现中的注意点
正如二叉搜索树中提到的那样(https://blog.csdn.net/weixin_62526435/article/details/123501482?spm=1001.2014.3001.5502),在树形数据结构里,如果发生了退化的情况,那么复杂度就会变得很高。因此,有必要想办法避免退化的发生。在并查集中,只需按照如下方法就可以避免退化。
- 对于每棵树,记录这棵树的高度(rank)
- 合并时如果两棵树的rank不同,那么从rank小的向rank大的连边。
此外,通过路径压缩,可以使得并查集更加有效。对于每个节点,一旦向上走到了一次根节点,就把这个点到父亲的边改为直接连向根。
在此之上,不仅仅是所查询的节点,在查询过程中向上经过的所有的节点,都改为直接连到根上。这样再次查询这些节点时,就可以很快知道根是谁了。
在使用这种简化的方法时,为了简单起见,即使树的高度发生了变化,我们也不修改rank的值。
4. 并查集的复杂度
加入了这两个优化之后的并查集效率非常高。对n个元素的并查集进行一次操作的复杂度是O(a(n))。在这里,a(n)是阿克曼函数的反函数。这比O(log(n))还要快。
不过,这是“均摊复杂度”。也就是说,并不是每一次操作都满足这个复杂度,而是多次操作之后平均每一次操作的复杂度是O(a(n))的意思。
5. 并查集的实现
下面是并查集的实现的例子。在例子中,我们用编号代表每个元素。数组par表示的是父亲的编号,par[x] = x 时,x是所在的树的根。
代码实现
#include <iostream>
#include <cstdio>
#include <map>
#define ll long long
using namespace std;
#define MAX_N 100
int par[MAX_N]; // 父亲
int ran[MAX_N]; // 树的高度
// 初始化n个元素
void init(int n){
for(int i=0;i<n;i++){
par[i]=i;
ran[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(ran[x]<ran[y]){
par[x]=y;
}
else{
par[y]=x;
if(ran[x]==ran[y]){
ran[x]++;
}
}
}
// 判断x和y是否属于同一个集合
bool same(int x,int y){
return find(x)==find(y);
}
6. 需要用到并查集的问题
食物链
问题描述
有N只动物,分别编号为1,2,…,N。所有动物都属于A,B,C中的其中一种。已知A吃B、B吃C、C吃A。按顺序给出下面的两种信息共K条。
- 第一种:x和y属于同一种类
- 第二种:x吃y
然而这些信息有可能会出错。有可能有的信息和之前给出的信息矛盾,也有的信息可能给出的x和y不在1,2,…,N的范围内。求在k条信息中有多少条是不正确的。计算过程中,我们将忽视诸如此类的错误信息。
⚠️限制条件
- 1≤N≤50000
- 0≤K≤100000
样例
输入
N=100,k=7
信息有下面7条
第一种,x=101,y=1
第二种,x=1,y=2
第二种,x=2,y=3
第二种,x=3,y=3
第一种,x=1,y=3
第二种,x=3,y=1
第二种,x=5,y=5
输出
3(第1、4、5条是错误的信息)
由于N和K很大,所以必须高效地维护动物之间的关系,并快速判断是否产生了矛盾。并查集是维护“属于同一组”的数据结构,但是在本题中,并不只有属于同一类的信息,还有捕食关系的存在。因此需要开动脑筋维护这些关系。
对于每只动物 i 创建3个元素 i-A,i-B,i-C,并用这3×N个元素建立并查集。这个并查集维护如下信息:
- i-x表示“i属于种类x”。
- 并查集里的每一个组表示组内所有元素代表的情况都同时发生或不发生。
例如,如果i-A和j-B在同一组组里,就表示如果i属于种类A那么j一定属于种类B,如果j属于种类B那么i一定属于种类A。因此,对于每一条信息,只需要按照下面进行操作就可以了。
- 第一种,x和y属于同一种类…合并x-A和y-A、x-B和y-B、x-C和y-C。
- 第二种,x吃y…合并x-A和y-B、x-B和y-C、x-C和y-A。
不过在合并之前,需要先判断合并是否会产生矛盾。例如在第一种信息的情况下,需要检查比如x-A和y-B或者y-C是否在同一组等信息。
参考代码
#include <iostream>
using namespace std;
#define MAX_N 100000
int N,K;
int T[MAX_N],X[MAX_N],Y[MAX_N];
int par[MAX_N]; // 父亲
int ran[MAX_N]; // 树的高度
// 查询树的根
int find (int x){
if(par[x]==x){
return x;
}
else{
return par[x]=find(par[x]);
}
}
bool same(int x,int y){
return find(x)==find(y);
}
void init(int n){
for(int i=0;i<n;i++){
par[i]=i;
ran[i]=0;
}
}
// 合并x和y所属的集合
void unite(int x,int y){
x=find(x);
y=find(y);
if(x==y){
return;
}
if(ran[x]<ran[y]){
par[x]=y;
}
else{
par[y]=x;
if(ran[x]==ran[y]){
ran[x]++;
}
}
}
void solve()
{
// 初始化并查集
// 元素X,X+N,X+2*N分别代表X-A,X-B,X-C
init(N*3);
int ans=0;
for(int i=0;i<K;i++){
int t=T[i];
int x=X[i]-1,y=Y[i]-1; // 把输入变成0,...,N-1的范围
// 不正确的编号
if(x<0||N<=x||y<0||N<=y){
ans++;
continue;
}
if(t==1){
// "x和y属于同一类"的信息
if(same(x,y+N)||same(x,y+2*N)){
ans++;
}
else{
unite(x,y);
unite(x+N,y+N);
unite(x+N*2,y+N*2);
}
}
else{
// x吃y的信息
if(same(x,y)||same(x,y+2*N)){
ans++;
}
else{
unite(x,y+N);
unite(x+N,y+2*N);
unite(x+2*N,y);
}
}
}
printf("%d\n",ans);
}
int main()
{
scanf("%d%d",&N,&K);
for(int i=0;i<K;i++){
scanf("%d%d%d",&T[i],&X[i],&Y[i]);
}
solve();
return 0;
}
输出样例
若有帮助的话,请点个赞吧!😊