今天学习了新的算法——并查集,感觉还挺好用。
并查集
介绍
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
有n各元素的集合问题中,我们要将他们按照一定的法则分成若干组,并且还要查寻任意两个元素在不在同一组。
基本思路是:先把这n各元素每个元素单独一组,然后按照一定的顺序把属于同一组的元素合并,最后的都若干个集合。
对于并查集通常来说,我们用根节点来表示一个集合。
当元素过多时查询时间可能会长,这是需要用路径压缩,让统一集合中的每个元素父节点都指向根节点。这样查询或变得更快/
对于一个并查集主要涉及两种操作,查询和合并
查询就是查找某一个元素所在的集合。
合并就是将两个元素所在的集合合并。
常见用途
一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。
基本代码
//查找某个元素所在的集合时,我们并不关心这个集合中有哪些元素,我们只需要知道要查找的元素的根节点是谁就行了。
//用uset[i] 表示元素i的父节点每次更新uset[i],使得uset[i]等于uset[i]的父节点, 最后uset[i]得到的就是元素i的根节点。这就是路径压缩。
const int MAXSIZE = 100010;
int uset[MAXSIZE]; // 表示并查集,uset[i] 代表第i个元素的父节点是谁
int rank[MAXSIZE]; // 该节点的秩,代表该节点下面有多少层
void inituset(int size) //初始化并查集,让每个元素的父节点都是自身,即每个元素单独一组
{
for(int i=1;i<=n;i++)
{
uset[i] = i;
rank[i] = 1; //初始时每个节点的父节点都是其自身,所以秩为1,即该棵树只有1层(根节点,也就是其本身)
}
}
int find(int x) //查找操作,用来查找一个元素所在的集合。(集合用根节点表示)
{
if(x != uset[x]) uset[x] = find(uset[x]); //当父节点是自身的时候说明已经是根节点了,根节点代表了这个集合。每次将uset[x] 的父节点在赋值给 uset[x] 就是所谓的路径压缩,你搜索完一次后,该集合中每个元素都的父节点都变成了根节点,下次在查找的时候递归的层数少了就会快很多。
return uset[x];
}
void merge(int a,int b) //合并操作,合并a,b两个元素所在的集合
{
if((a = find(a)) == (b = find(b))) return; //如果a,b的根节点相同说明在同一个集合中,不用合并
if(rank[a] > rank[b]) uset[b] = a;
else
{
uset[a] = b;
if(rank[a] == rank[b]) rank[b]++;
}
// 众所周知,并查集使用树来表示的,我们每次可以将秩小的树接在秩大的树下面,这能能保证每次操作后得到的树的秩较小,在查找操作时就能节省很多时间。
//如果你想知道最后还剩多少个集合的话,遍历一遍uset[i] 就行了
int count - 0;
for(int i=1;i<=n;i++)
{
if(uset[i] == i) count++;
}
//最后剩下了count个独立的集合(很多题都是求这个)
}
例题
这些事非常简单的例题,主要用于理解并查集。
这三道题都是nowcoder上面的⭐题,(⭐越多难度越大,一星难度最低。)他们只涉及并查集,不涉及其他数据结构和算法。适合学习并查集时练习。
1. 加边的无向图
题目链接:[并查集] 加边的无向图
题目描述
给你一个 n 个点,m 条边的无向图,求至少要在这个的基础上加多少条无向边使得任意两个点可达~
输入
第一行两个正整数 n 和 m 。
接下来的m行中,每行两个正整数 i 、 j ,表示点i与点j之间有一条无向道路。
输出
输出一个整数,表示答案
思路
建立并查集,每读入一次i,j就把i和j所在的集合合并,最后统计还有多少个独立的集合count。 任意两个集合之间任选两点加一条边就能联通,所以总的需要加的边数为count -1;
AC代码
#include <iostream>
using std::ios;
using std::cin;
using std::cout;
const int MAXSIZE = 100010;
int uset[MAXSIZE];
int rank[MAXSIZE];
void makeuset(int size)
{
for(int i=1;i<=size;i++)
{
uset[i]=i;
rank[i]=1;
}
}
int find(int x)
{
if(x != uset[x]) uset[x] = find(uset[x]);
return uset[x];
}
void unionuset(int a,int b)
{
if((a = find(a)) == (b = find(b))) return ;
if(rank[a]>rank[b]) uset[b] = a;
else{
uset[a] = b;
if(rank[a] == rank[b]) rank[b]++;
}
}
int main()
{
int n,m,x,y;
scanf("%d%d",&n,&m);
makeuset(n);
for(int i=0;i<m;i++)
{
scanf("%d%d",&x,&y);
unionuset(x,y);
}
int count = 0;
for(int i=1;i<=n;i++) if(uset[i] == i) count++;
printf("%d\n",count-1);
return 0;
}
2. Call to your teacher
题目链接:[并查集] Call to your teacher
题目描述
从实验室出来后,你忽然发现你居然把自己的电脑落在了实验室里,但是实验室的老师已经把大门锁上了。更糟的是,你没有那个老师的电话号码。你开始给你知道的所有人打电话,询问他们有没有老师的电话,如果没有,他们也会问自己的同学来询问电话号码。那么,你能联系到老师并且拿到电脑吗。
输入
存在多组测试样例
每组样例的第一行分别是两个整数n(1<n<=50),m(1<m<=2000),n是在题目当中出现的人数,其中你的序号是1号,实验室老师的序号是n。
接下来的m行,每行有两个整数x(1<=x<=n),y(1<=y<=n),代表x有y的电话号码。
输出
对于每组测试样例,如果你最终能联系到老师,输出“Yes”,否则输出“No”。
思路
建立并查集,每输入一组x,y便将x,y所在的集合合并一次,最后查找1和n是不是在同一个集合中 find(1) == find(2) ?。(需要注意的是如果老师有其他同学的电话是联系不到老师的)
AC代码
#include <iostream>
using std::ios;
using std::cin;
using std::cout;
const int MAXSIZE = 55;
int uset[MAXSIZE];
int rank[MAXSIZE];
void makeuset(int size)
{
for(int i=1;i<=size;i++)
{
uset[i] = i;
rank[i] = 1;
}
}
int find(int x)
{
if(x != uset[x]) uset[x] = find(uset[x]);
return uset[x];
}
void merge(int a,int b)
{
if((a = find(a)) == (b = find(b))) return;
if(rank[a] > rank[b]) uset[b] = a;
else
{
uset[a] = b;
if(rank[a] == rank[b]) rank[b]++;
}
}
int main()
{
int n,m,x,y;
scanf("%d%d",&n,&m);
makeuset(n);
for(int i=0;i<m;i++)
{
scanf("%d%d",&x,&y);
if(x != n)merge(x,y);
}
if(find(1) == find(n)) printf("Yes\n");
else printf("No\n");
return 0;
}
3. 任意点
题目链接:【并查集】 任意点
题目描述
平面上有若干个点,从每个点出发,你可以往东南西北任意方向走,直到碰到另一个点,然后才可以改变方向。
请问至少需要加多少个点,使得点对之间互相可以到达。
输入
第一行一个整数n表示点数( 1 <= n <= 100)。
第二行n行,每行两个整数xi, yi表示坐标( 1 <= xi, yi <= 1000)。
y轴正方向为北,x轴正方形为东。
输出
输出一个整数表示最少需要加的点的数目。
思路
建立并查集,如果输入的两个点中有相同的x或者相同的y那么这两个点属于同一个集合。最后统计还剩下多少个独立的集合count ,任意两个集合间加一个点便能联通。所以所需总点数为 count - 1;
AC代码
#include <iostream>
using std::cin;
using std::cout;
const int MAXSIZE = 110;
struct point
{
int x,y;
};
int uset[MAXSIZE];
int rank[MAXSIZE];
point pos[MAXSIZE];
void makeuset(int size)
{
for(int i=1;i<=size;i++) uset[i] = i;
for(int i=1;i<=size;i++) rank[i] = 1;
}
int find(int x)
{
if(x != uset[x]) uset[x] = find(uset[x]);
return uset[x];
}
void unionuset(int a,int b)
{
if((a = find(a)) == (b = find(b))) return;
if(rank[a] > rank[b]) uset[b] = a;
else{
uset[a] = b;
if(rank[a] == rank[b]) rank[b]++;
}
}
bool isequal(point a,point b)
{
if(a.x == b.x || a.y == b.y) return true;
else return false;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&pos[i].x,&pos[i].y);
makeuset(n);
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(isequal(pos[i],pos[j])) unionuset(i,j);
}
}
int count = 0;
for(int i=1;i<=n;i++) if(i == uset[i]) count++;
printf("%d\n",count-1);
return 0;
}