并查集
并查集是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。并查集包括如下两个基本操作:
- Get,查询一个元素属于哪一个集合。
- Merge,把两个集合合并成一个大集合。
并查集的操作
- 并查集的存储
使用一个数组 fa 保存父节点(根的父节点设为自己)。
int fa[SIZE];
- 并查集的初始化
设有 n 个元素,起初所有元素各自构成一个独立的集合,即有 n 棵 1 个点的树。
for(int i = 1; i <= n; i++)
fa[i] = i;
- 并查集的 Get 操作
若 x 是树根,则 x 就是集合代表,否则递归访问 fa[x] 直至根节点。
int get(int x){
if(x == fa[x])
return x;
return fa[x] = get(fa[x]);//路径压缩,fa 直接赋值为代表元素
}
路径压缩:在每次执行 Get 操作的同时,把访问得到每个节点(也就是所查询元素的全部祖先)都直接指向树根。如下图:
- 并查集的 Merge 操作
合并元素 x 和元素 y 所在的集合,等价于让 x 的树根作为 y 的树根的子节点。
void merge(int x, int y){
fa[get(x)] = get(y);
}
并查集图解
图解
当下社会形式不断内卷,许多的学生组成了内卷团伙,使内卷更加有效果。由于内卷人数过于庞大,内卷频度高,学校反内卷处想查清楚到底有几个内卷团伙,反卷处搜集到了一些线索。
现有 7 个卷王。
1 号卷王与 2 号卷王是团伙。
3 号卷王与 4 号卷王是团伙。
5 号卷王与 2 号卷王是团伙。
4 号卷王与 6 号卷王是团伙。
2 号卷王与 6 号卷王是团伙。
1 号卷王与 6 号卷王是团伙。
卷王的团伙的团伙也是团伙。
首先我们假设这 7 个卷王互相是不认识的,我们各自为政,每个人都是头头,他们都是自己卷。我们通过线索一步步合并团伙。
第一步:申请一个一维数组 fa,用 fa[1]~fa[7] 分别存储 1~7 号卷王中每个卷王的头头是谁。
第二步:初始化。7 个卷王各自为战,每个卷王的头头就是自己。“1 号卷王”的头头就是“1 号卷王”。以此类推,“7 号卷王”的头头是“7 号卷王”,即 fa[7] 的值为 7。
第三步:开始合并团伙。
第一条线索:1 号卷王与 2 号卷王是团伙。(我们规定左边为大,所以 1 号卷王是卷王的头头)
第二条线索:3 号卷王与 4 号卷王是团伙。
第三条线索:5 号卷王与 2 号卷王是团伙。这个时候就出现了一个矛盾 2 号卷王应该归顺 5 号还是 1 号。这是 5 号可以使 2 号的头 1 号归顺自己,那么 2 号也就是归顺自己的,即将 fa[1] 的值改为 5。
第四条线索:4 号卷王与 6 号卷王是团伙。fa[4] 的值是 3,fa[6] 的值是 6。所以让 6 号卷王加入 3 号卷王团伙,即将 fa[6] 的值改为 3。
第五条线索:2 号卷王与 6 号卷王是团伙。fa[2] 的值是 1,fa[1] 的值是 5,即 2 号卷王的大头头是 5 号卷王。fa[6] 的值是 3,6 号卷王的头头是 3 号卷王。我们让 6 号卷王的头头 3 号卷王归顺 2 号卷王的大头头 5 号卷王。 即将 fa[3] 的值改为 5,也就是 3 号卷王带着手下的卷王都归顺了 5 号卷王。
第六条线索:1 号卷王与 6 号卷王是团伙。1 号卷王与 2 号卷王的大头头都是 5 号卷王。这又是一次路径压缩的过程。6 号卷王在寻找大头头 5 号卷王的时候,被大头头 5 号卷王直接领导了。
所有线索分析完毕,看看有几个团伙?如果 fa[i]=i,就表示此人是一个卷王团伙的最高领导人。最后数组中 fa[5]=5、fa[7]=7
代码
#include<stdio.h>
int fa[1001]={0},n,m,sum=0;
void init(){//这里是初始化,非常地重要存的是自己数组下标的编号就好
for(int i = 1; i <= n; i++)
fa[i] = i;
return;
}
int getfa(int v){//找最大头头的递归函数
if(fa[v] == v)
return v;
else{
fa[v] = getfa(fa[v]);//路径压缩
return fa[v];
}
}
void merge(int v, int u){
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2){//判断v,u是否在一个集合中
fa[t2] = t1;//把左边的当成领导
}
return;
}
int main(){
scanf("%d %d",&n,&m);
int x,y;
init();//初始化
for(int i = 1; i <= m; i++){
scanf("%d %d",&x,&y);
merge(x,y);//合并团伙
}
for(int i = 1; i <= n; i++)//检测有多少个团伙
if(fa[i] == i)
sum++;
printf("%d\n",sum);
return 0;
}
例题
第一题
输入
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出
Yes
Yes
No
#include<stdio.h>
int fa[10001]={0},n,m,p;
void init(){//这里是初始化,非常地重要存的是自己数组下标的编号就好
for(int i = 1; i <= n; i++)
fa[i] = i;
return;
}
int getfa(int v){//找最大头头的递归函数
if(fa[v] == v)
return v;
else{
fa[v] = getfa(fa[v]);//路径压缩
return fa[v];
}
}
void merge(int v, int u){
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2){//判断v,u是否在一个集合中
fa[t2] = t1;//把左边的当成领导
}
return;
}
int find(int v, int u){
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2)
return 0;
return 1;
}
int main(){
scanf("%d %d %d",&n,&m,&p);
int x,y;
init();//初始化
for(int i = 1; i <= m; i++){
scanf("%d %d",&x,&y);
merge(x,y);//合并团伙
}
for(int i = 1; i <= p; i++){
scanf("%d %d",&x,&y);
if(find(x,y) == 1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
第二题
输入
3
2 4 1
0 0 1
0 0 3
2 5 1
0 0 1
0 0 4
2 5 2
0 0 2
2 0 4
输出
Yes
No
Yes
#include<stdio.h>
int fa[10001]={0},n,h;
long long r,x[100001],y[100001],z[100001];
int f1[100001],f2[100001];//f1记录与顶面相交的序号,f2记录与 底面相交的序号
void init(){//这里是初始化,非常地重要存的是自己数组下标的编号就好
for(int i = 1; i <= n; i++)
fa[i] = i;
return;
}
int getfa(int v){//找最大头头的递归函数
if(fa[v] == v)
return v;
else{
fa[v] = getfa(fa[v]);//路径压缩
return fa[v];
}
}
void merge(int v, int u){//合并v,u
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2){//判断v,u是否在一个集合中
fa[t2] = t1;//把左边的当成领导
}
return;
}
int find(int v, int u){//v,u是否在一个集合
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2)
return 0;
return 1;
}
long long dis(long long x, long long y, long long z, long long x1, long long y1, long long z1){//计算距离
return (x-x1)*(x-x1)+(y-y1)*(y-y1)+(z-z1)*(z-z1);
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d %d %lld",&n,&h,&r);
init();
int t1 = 0, t2 = 0;//t1记录与顶面相交的洞,t2记录与底面相交的洞
for(int i = 1; i <= n; i++){
scanf("%lld %lld %lld",&x[i],&y[i],&z[i]);
if(z[i]+r >= h){//该圆是否与顶面相交
t1 ++;
f1[t1] = i;
}
if(z[i]-r <= 0){//该圆是否与底面相交
t2 ++;
f2[t2] = i;
}
for(int j = 1; j <= i; j++){//枚举之前的洞是否于这个洞相交
if(dis(x[i],y[i],z[i],x[j],y[j],z[j]) <= 4*r*r){//相交,则合并集合
merge(i,j);
}
}
}
int flag = 0;
for(int i = 1; i <= t1; i++){//看看是否有连通的洞
for(int j = 1; j <= t2; j++)
if(find(f1[i],f2[j]) == 1){
flag = 1;
break;
}
if(flag == 1)
break;
}
if(flag)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
第三题
输入
5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17
输出
5
#include<stdio.h>
int fa[1000001]={0},n,m,k;
void init(){//这里是初始化,非常地重要存的是自己数组下标的编号就好
for(int i = 1; i <= n*m; i++)
fa[i] = i;
return;
}
int getfa(int v){//找最大头头的递归函数
if(fa[v] == v)
return v;
else{
fa[v] = getfa(fa[v]);//路径压缩
return fa[v];
}
}
void merge(int v, int u){//合并v,u
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2){//判断v,u是否在一个集合中
fa[t2] = t1;//把左边的当成领导
}
return;
}
int find(int v, int u){//v,u是否在一个集合
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2)
return 0;
return 1;
}
int main(){
scanf("%d %d %d",&m,&n,&k);
init();
for(int i = 0; i < k; i++){
int a,b;
scanf("%d %d",&a,&b);
merge(a,b);
}
int sum = 0;
for(int i = 1; i <= n*m; i++)
if(fa[i] == i)
sum ++;
printf("%d",sum);
return 0;
}
第四题
题目链接
采用逆向修建桥的思路,我们根据桥的有效天数进行递减排序。然后依次进行建桥,若该天建的桥的两个岛原本不连通,则该天的后一天会出现争议。
#include<stdio.h>
#include<stdlib.h>
struct Bridge{
int x,y;
int day;
Bridge(){}
Bridge(int a, int b, int c):x(a),y(b),day(c){}
};
Bridge bridge[100010];//存储所有桥
int fa[100010]={0},n,m;//存储岛的上级
void init(){//这里是初始化,非常地重要存的是自己数组下标的编号就好
for(int i = 1; i <= n; i++)
fa[i] = i;
return;
}
int getfa(int v){//找最大头头的递归函数
if(fa[v] == v)
return v;
else{
fa[v] = getfa(fa[v]);//路径压缩
return fa[v];
}
}
int merge(int v, int u){//合并v,u
int t1 = getfa(v), t2 = getfa(u);//获得v,u的最大头头
if(t1 != t2){//判断v,u是否在一个集合中
fa[t2] = t1;//把左边的当成领导
return 1;
}
return 0;
}
int cmp(const void *a, const void *b){//排序规则
struct Bridge *aa = (Bridge *)a;
struct Bridge *bb = (Bridge *)b;
return ((aa->day) > (bb->day)) ? -1:1;
}
int main(){
scanf("%d %d",&n,&m);
init();
for(int i = 1; i <= m; i++){
int a,b,t;
scanf("%d %d %d",&a,&b,&t);
bridge[i] = Bridge(a,b,t);
}
qsort(bridge+1,m,sizeof(bridge[1]),cmp);//day递减,快排
int sum = 0, lastDay = 0;
for(int i = 1; i <= m; i++){
if(merge(bridge[i].x,bridge[i].y) == 1 && bridge[i].day != lastDay){//岛x、y未连通,此桥的天数是第一次出现
sum++;
lastDay = bridge[i].day;
}
}
printf("%d",sum);
return 0;
}