【数据结构】并查集

文章目录

  • [1. 概述](#1. 概述)
  • [2. 原理](#2. 原理)
  • [3. 实现](#3. 实现)
    • [3.1 类结构设计](#3.1 类结构设计)
    • [3.2 构造函数(初始化)析构函数(释放资源)](#3.2 构造函数(初始化)析构函数(释放资源))
    • [3.3 查找对应元素的代表元](#3.3 查找对应元素的代表元)
    • [3.4 判断两个元素是否处于同一个集合](#3.4 判断两个元素是否处于同一个集合)
    • [3.5 合并两个元素](#3.5 合并两个元素)
    • [3.6 查看整体数据中的集合数](#3.6 查看整体数据中的集合数)
    • [3.7 测试代码](#3.7 测试代码)
    • [3.8 路径压缩](#3.8 路径压缩)
  • [4. 应用](#4. 应用)

1. 概述

故事引入:

话说在江湖中散落着各式各样的大侠,他们怀揣着各自的理想和信仰在江湖中奔波。或是追求武林至尊,或是远离红尘,或是居庙堂之高,或是处江湖之远。尽管大多数人都安分地在做自己,但总有些人会因为彼此的信仰不同而聚众斗殴。因此,江湖上常年乱作一团,纷纷扰扰。

这样长期的混战,难免会打错人,说不定一刀就把拥有和自己相同信仰的队友给杀了。这该如何是好呢?于是,那些有着相同信仰的人们便聚在一起,进而形成了各种各样的门派,比如我们所熟知的"华山派"、"峨嵋派"、",崆峒派"、"少林寺"、"明教"......这样一来,那些有着相同信仰的人们便聚在一起成为了朋友。以后再遇到要打架的事时,就不会打错人了。

但是新的问题又来了,原本互不相识的两个人如何辨别是否共属同一门派呢?

这好办!我们可以先在门派中选举一个"大哥"作为话事人(也就是掌门人,或称教主等)。这样一来,每当要打架的时候,决斗双方先自报家门,说出自己所在门派的教主名称,如果名称相同,就说明是自己人,就不必自相残杀了,否则才能进行决斗。于是,教主下令将整个门派划分为三六九等,使得整个门派内部形成一个严格的等级制度(即树形结构)。教主就是根节点,下面分别是二级、三级、......、N级队员。每个人只需要记住自己的上级名称,以后遇到需要辨别敌友的情况时,只需要一层层往上询问(网上询问)就能知道是否是同道中人了。

数据结构的角度来看:

由于我们的重点是在关注两个人是否连通,因此他们具体是如何连通的,内部结构是怎样的,甚至根节点是哪个(即教主是谁),都不重要。所以并查集在初始化时,教主可以随意选择(就不必再搞什么武林大会了),只要能分清敌友关系就行。

备注:上面所说的"教主"在教材中被称为"代表元"。即:用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元。用树形结构的术语来说的话,就是这棵树的


总结:在实际的应用场景中,我们经常会遇到一些问题,需要将n个不同的元素划分成一些不相交的集合,然后按照一定的规律将有一些相同共性的集合进行合并(并),需要一些操作快速的查找某些元素是否在同一个集合(查),为了能够高效的实现这些操作,设计出了一种数据结构叫做并查集(Union Find Set)

2. 原理

对于上述的需求,我们设计出了并查集这个数据结构,但是实际上可以给这个数据结构进行一些优化的设置

我们将使用数组作为底层结构来存放数据,假设一共有n个元素,那么就创建n个元素的数组,首先我们将所有下标对应的值设置为-1,表示他们单独成为一个集合(门派)对于属于同一集合的元素,在其对应位置存放他的上级的下标,以此类推

按照上面的场景,我们将有以下的需求:

  1. 判断两个元素是否处于同一集合
  2. 合并两个元素所在的集合
  3. 判断整体有多少个集合
  4. 查找对应元素的代表元

3. 实现

3.1 类结构设计

cpp 复制代码
class UnionFindSet
{
private:
	vector<int> _ufs; // 成员变量使用一个数组即可
public:
	UnionFindSet(); // 构造函数
	~UnionFindSet(); // 析构函数
	bool IsSameSet(int x, int y); // 判断是否处于同一个集合
	void Union(int x, int y); // 合并两个元素所在集合
	size_t Count(); // 查看整体数据中的集合数
	int FindRoot(int x); // 查找对应元素的代表元
};

3.2 构造函数(初始化)析构函数(释放资源)

构造函数可以根据实际情况更改或者重载,这里只实现根据整体的元素个数来构造

cpp 复制代码
UnionFindSet(int n) :_ufs(n, -1) {}
// 析构函数,不需要析构,vector会自动调用他的析构函数

3.3 查找对应元素的代表元

我们知道对于属于同一集合的元素,在其对应位置存放他的上级的下标,代表元也就是说他本身没有上级(存放元素内容为-1),然后任何一个元素一直向上级查找,最终都能找到代表元

cpp 复制代码
int FindRoot(int x) // 查找对应元素的代表元
{
    int root = x;
    while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
    {
        root = _ufs[root]; // 找到当前位置的上级
    }
    return root;
}

3.4 判断两个元素是否处于同一个集合

如果两个元素拥有同一个代表元,那么就证明他们属于同一个集合

cpp 复制代码
bool IsSameSet(int x, int y) // 判断两个元素是否处于同一个集合
{
    int root1 = FindRoot(x); // 分别查找两个元素的代表元
    int root2 = FindRoot(y);
    return root1 == root2;
}

3.5 合并两个元素

要合并连个元素,不能够直接合并这两个元素,而是应该合并两个元素所在的两个集合,所以需要先找到对应集合的代表元,然后将其中一个集合的代表元设置为另一个集合的代表元的下级

cpp 复制代码
void Union(int x, int y) // 合并两个元素所在集合
{
    // 分别查找两个元素的代表元
    int root1 = FindRoot(x); 
    int root2 = FindRoot(y);
    if (root1 == root2) // 如果两个元素本来就属于同一个集合,就直接return
        return;
    // 这里假设root1为合并后的代表元,
    //	1. 让root1存放的内容更改为整体
    _ufs[root1] += _ufs[root2];
    //	2. 更改root2内存放的值(root2现在是root1的下级)
    _ufs[root2] = root1; // 所以要存放root1的下标
}

3.6 查看整体数据中的集合数

根据我们的设计,所有的代表元存放的值都是负数,所以集合的个数 == 代表元的个数 == 值为负数的元素个数

cpp 复制代码
size_t Count() // 查看整体数据中的集合数
{
    size_t count = 0;
    for (auto& e : _ufs)
    {
        if (e < 0) 
            count++;
    }
    return count;
}

3.7 测试代码

cpp 复制代码
// 这里是一个简单的测试,读者朋友们编写完代码后可以自行测试
void Test1()
{
	UnionFindSet ufs(10);
	cout << ufs.Count() << endl;
	ufs.Union(0, 9);
	ufs.Union(1, 8);
	ufs.Union(2, 7);
	ufs.Union(3, 6);
	ufs.Union(4, 5);
	
	ufs.Union(0, 1);
	cout << ufs.Count() << endl;
	cout << ufs.FindRoot(9) << endl;
	cout << ufs.FindRoot(8) << endl;
	cout << ufs.IsSameSet(8, 9) << endl;
	cout << ufs.IsSameSet(5, 9) << endl;
}

3.8 路径压缩

在一些对效率要求比较高的地方,我们可能会采取一些优化的方式,让查找的效率变得更高。经过分析可以发现,查找代表元的过程效率跟下级的个数(树的层数)有关,但是我们不关心同一个集合的上下级关系,所以可以让集合内除代表元之外的所有元素都是代表元的直接下级,这样查找的效率就变成了O(1),可是每一次都这样更新是比较复杂并且消耗时间资源的,所以没有必要单独更新,可以把更新的行为和查找放在一起

只要查找一次,就将查找路径上的所有结点都挂到根结点下面,如图,查找L的根结点A,查找一次过后,就将E、B、L全部挂到根结点A之下

cpp 复制代码
int Find1(int x) // 路径压缩1
{
    int root = x;
    while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
    {
        root = _ufs[root];
    }
    while (x != root) //x不为根结点,则压缩路径
    {
        int t = _ufs[x]; //t指向x的父节点
        _ufs[x] = root; //x直接挂到根结点下
        x = t;
    }
    return root;
}

扩展:加权标记法优化


上面代码汇总

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;


class UnionFindSet
{
private:
	vector<int> _ufs; // 成员变量使用一个数组即可
public:
	UnionFindSet(int n) :_ufs(n, -1) {}
	// 析构函数,不需要析构,vector会自动调用他的析构函数
	bool IsSameSet(int x, int y) // 判断两个元素是否处于同一个集合
	{
		int root1 = FindRoot(x); // 分别查找两个元素的代表元
		int root2 = FindRoot(y);
		return root1 == root2;
	}
	void Union(int x, int y) // 合并两个元素所在集合
	{
		// 分别查找两个元素的代表元
		int root1 = FindRoot(x); 
		int root2 = FindRoot(y);
		if (root1 == root2) // 如果两个元素本来就属于同一个集合,就直接return
			return;
		// 这里假设root1为合并后的代表元,
		//	1. 让root1存放的内容更改为整体
		_ufs[root1] += _ufs[root2];
		//	2. 更改root2内存放的值(root2现在是root1的下级)
		_ufs[root2] = root1; // 所以要存放root1的下标
	}
	size_t Count() // 查看整体数据中的集合数
	{
		size_t count = 0;
		for (auto& e : _ufs)
		{
			if (e < 0) 
				count++;
		}
		return count;
	}
	int FindRoot(int x) // 查找对应元素的代表元
	{
		int root = x;
		while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
		{
			root = _ufs[root]; // 找到当前位置的上级
		}
		return root;
	}
	int Find1(int x) // 路径压缩1
	{
		int root = x;
		while (_ufs[root] >= 0) // 循环查找,直到找到存放元素为负数的情况
		{
			root = _ufs[root];
		}
		while (x != root) //x不为根结点,则压缩路径
		{
			int t = _ufs[x]; //t指向x的父节点
			_ufs[x] = root; //x直接挂到根结点下
			x = t;
		}
		return root;
	}
};


void Test1()
{
	UnionFindSet ufs(10);
	cout << ufs.Count() << endl;
	ufs.Union(0, 9);
	ufs.Union(1, 8);
	ufs.Union(2, 7);
	ufs.Union(3, 6);
	ufs.Union(4, 5);
	
	ufs.Union(0, 1);
	cout << ufs.Count() << endl;
	cout << ufs.FindRoot(9) << endl;
	cout << ufs.FindRoot(8) << endl;
	cout << ufs.IsSameSet(8, 9) << endl;
	cout << ufs.IsSameSet(5, 9) << endl;
}

int main()
{
	Test1();

	return 0;
}

4. 应用

下面是两个leetcode的题目,感兴趣的同学可以做一做,练习一下并查集的相关操作

LCR 116. 省份数量

990. 等式方程的可满足性


参考博文


本节完...

相关推荐
郭wes代码1 分钟前
Cmd命令大全(万字详细版)
python·算法·小程序
scan72416 分钟前
LILAC采样算法
人工智能·算法·机器学习
菌菌的快乐生活36 分钟前
理解支持向量机
算法·机器学习·支持向量机
大山同学41 分钟前
第三章线性判别函数(二)
线性代数·算法·机器学习
axxy20001 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
黑客Ash1 小时前
安全算法基础(一)
算法·安全
AI莫大猫2 小时前
(6)YOLOv4算法基本原理以及和YOLOv3 的差异
算法·yolo
taoyong0012 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
Uu_05kkq2 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
清梦20203 小时前
经典问题---跳跃游戏II(贪心算法)
算法·游戏·贪心算法