[算法专题] 并查集入门

今天学习了新的算法——并查集,感觉还挺好用。

并查集

介绍

并查集是一种树型的数据结构,用于处理一些不相交集合(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;
}

猜你喜欢

转载自blog.csdn.net/qq_30445397/article/details/107528642