并查集(解释和C++模板)

文章目录

  • 前言
  • 一、结构定义及初始化
  • 二、查找结点所在集合根节点
  • 三、合并
  • 四、路径压缩解释
  • 五、整体模板
  • 总结


前言

并查集是一种树形的数据结构,顾名思义,它用于处理一些不交集的合并及查询问题(可以判断两个结点之间是否连通)。 它支持两种操作:

查找(Find):确定某个元素处于哪个子集;
合并(Union): 将两个集合合并一个集合


提示:以下是本篇文章正文内容,下面案例可供参考

一、结构定义及初始化

假定初始有N个结点,每一个结点都是独立。那么,怎么表示独立呢。我们定义一个father数组,表示每个结点所在集合的根节点,因为刚开始都是独立的,所以每个数组元素的值都是它自己的下标。举个例子:0号结点father[0] = 0, 1号结点father[1] = 1…依次类推,来进行初始化。如下图(以6个结点为例),刚开始每个结点状态,我们先对他们进行初始化。

class uonionFind{
	public:
		vector<int> father; //每个结点的所在集合的根结点 
		vector<int> size;  //每个结点所在集合的结点数,因为要查找根结点,所以只需要维护根所在的下标就行 
		int setCount;  //当前连通分量的个数 
		int n;
		//并查集初始化 
		unionFind(int _n): father(_n), size(_n, 1), n(_n), setCount(_n) {
  					  iota(father.beigin(), father.end(), 0);
		}
};

二、查找结点所在集合根节点

	int find(int x) {
			if(father[x] == x) return x;
			//路径压缩:让每个结点直接与根结点相连而不是间接相连,如果间接相连很可能树的深度很大,查找时间复杂度会大。
			return father[x] = find(father[x]);
		}

三、合并

每次操作时根据实际情况我们都会将两个结点连在一起:例如:
1.结点2和4连在一起,先查找他们的根节点:father[2] = 2, father[4] = 4,发现他们没有连通,所以合并(father[2] = 4):
在这里插入图片描述

2.结点0和4连在一起(因为father[0] = 0, father[4] = 4, 所以两个结点相连)father[0] = 4。
3.结点2和0连在一起,先查找他们两个的根结点发现都是4,所以他们已经相连不用再进行操作了。以下是合并的代码:
在这里插入图片描述

bool unite(int x, int y) {
			//查找X和y结点的所在集合的根节点 
			x = find(x);
			y = find(y);
			//如果两个结点已经在相连 
			if(x == y) {
				cout << x <<"和"<<y<<"已经相连"<<endl;
				return false; 
			}
		 	//优先将小的集合放到大的集合 
		 	if(size[x] <= size[y]) swap(x, y);
		 	father[y] = father[x];
		 	//修改集合元素个数 
			 size[x] += size[y];
			 --setCount; //连通分量数减一 
			return true;
		}

四、路径压缩解释

如果在查找代码中使用以下代码

int find(int x) {
	if(father[x] == x) return x;
	return find(father[x]);
}

举个例子:
并查集(解释和C++模板)_第1张图片

这是相连结点关系,
结点0, 2, 4所在集合的根结点为3
当我们查找的时候:查找0的时候需要3次,查找2的时候需要2次;当数据大的时候,时间复杂度最坏情况为O(N),因为在查找的时候一直是这个关系。
而当我们使用路径压缩时:当我们在查找的过程中就会变成:
并查集(解释和C++模板)_第2张图片
这样在下次查找的时候就可以在时间复杂度O(1)的情况下找到。

五、整体模板

class uonionFind{
	public:
		vector<int> father; //每个结点的所在集合的根结点 
		vector<int> size;  //每个结点所在集合的结点数,因为要查找根结点,所以只需要维护根所在的下标就行 
		int setCount;  //当前连通分量的个数 
		int n;
		//并查集初始化 
		unionFind(int _n): father(_n), size(_n, 1), n(_n), setCount(_n) {
  					  iota(father.beigin(), father.end(), 0);
		}
		
		//查找 
		int find(int x) {
			if(father[x] == x) return x;
			return father[x] = find(father[x]);
		} 
		
		
		bool unite(int x, int y) {
			//查找X和y结点的所在集合的根节点 
			x = find(x);
			y = find(y);
			//如果两个结点已经在相连 
			if(x == y) {
				cout << x <<"和"<<y<<"已经相连"<<endl;
				return false; 
			}
		 	//优先将小的集合放到大的集合 
		 	if(size[x] <= size[y] ) swap(x, y);
		 	father[y] = father[x];
		 	//修改集合元素个数 
			 size[x] += size[y];
			 --setCount; //连通分量数减一 
			return true;
		}
};

总结

每次使用时都需要查找此结点的根节点,并进行路径压缩,因为如果两个集合相连时,在合并操作只有根节点相连,他们各自集合的孩子结点都还是他们各自两个,如果直接调用father来判断是否已经连通,则会出错。
例题:
leetcode 547 省份数量
leetcode 684 冗余连接
leetcode 721 账户合并
leetcode 785 判断二分图
leetcode 1631 最小体力消耗路径
leetcode 1202 交换字符串中的元素

你可能感兴趣的:(数据结构)