高阶数据结构——并查集

news2024/9/23 15:28:46

1. 并查集的介绍

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。

在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)。

比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,武汉招3人,10个人来自不同的学校,起先互不相识,每个学生都是一个独立的小团体,我们给这10个人进行编号。

每个地方的学生自发组织成小分队一起上路,于是:西安的4名同学(编号为0,6,7,8)组成了一个团体,成都的三名同学(编号为1,4,9)组成了一个团体,武汉的3名同学(编号为2,3,5)组成了一个团体。假设由编号最小的同学担任队长。

有了这个结构之后,我们很容易就能判断出来某两位同学是否属于同一个团体,并且合并两个集合也非常方便,这就是并查集的价值所在。

2. 并查集的原理

在实际中我们可以通过一个数组来实现并查集,还是上面那个例子。我们定义一个数组,这个数组的下标就是每个成员的编号,数组中的值有两层意思:

1.对队长(根节点)来说,数组中保存这个集合中所有元素的个数 * -1。

2.对成员(除了根节点的其他结点)来说,数组中保存的是他的父节点的下标

所以数组中值 < 0 的都是根节点,根节点得绝对值是这个集合的元素个数。

最初一开始所有同学都是一个独立的团体,所以先初始化为 -1。

后续相同城市的同学组成了一个团体。

物理结构图如下:

逻辑结构图如下:

并查集可以用来解决哪些问题?

1.查找元素属于哪个集合

由于数组中保存的是他父节点的下标,所以我们可以根据这个特点,一路向上查找,直到找到一个是值为负数的结点即可

2.查找两个元素是否属于同一集合

沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在

3.将两个集合合并成一个集合

需要做以下两步

  • 将两个集合中的元素合并
  • 将一个集合名称改成另一个集合的名称

比如上面的例子,{0,6,7,8}集合和{1,4,9}进行合并。

注意观察数组的变化,1号由原来的-3变成0,0号由原来的-4变成了-7。

4.集合的个数

遍历数组,数组中元素为负数的个数即为集合的个数。

3. 并查集的实现

并查集的实现通常包含如下接口:

  • 1.查找父节点下标
  • 2.合并两个集合
  • 3.返回集合的个数
  • 4.判断两个值是否在同一个集合中
class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	//返回父节点的下标
	size_t FindRoot(size_t posi);
    //合并两个集合
	bool Union(size_t x1, size_t x2);
    //返回集合的个数
	size_t Count();
    //判断两个值是否在同一个集合中
    bool InSameSet(size_t x1, size_t x2);


private:
	vector<int> _ufs;
};

并查集的特点:

1.数组的下标对应集合中元素的编号

2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数

3. 数组中如果为非负数,代表该元素双亲在数组中的下标

3.1 找到根节点

size_t FindRoot(size_t posi)
{
	while (_ufs[posi] >= 0)
	{
		posi = _ufs[posi];
	}

	return posi;
}

查找思路:如果一个结点是根节点,那么直接返回下标即可,如果不是根节点,那么就一路向上查找,直到找到根。

以上图为例,比如我们要查找9的根。根据数组我们可以看到9的父节点是1。继续向上查找,1号的父节点是0,到了0号,我们发现数据中保存的是-7,一个小于0的数,表示这个结点就是一个父节点。所以我们最终得到了9的根节点是0号。

3.2 合并两个集合

bool Union(size_t x1, size_t x2)
{
	int root1 = FindRoot(x1);
	int root2 = FindRoot(x2);

	// 如果他们本身就在同一个集合中
	if (root1 == root2)
		return false;

	// 默认将root1为较大的集合
	if (_ufs[root1] > _ufs[root2])
		swap(root1, root2);

	//将root2合并到root1下
	_ufs[root1] += _ufs[root2];
	_ufs[root2] = root1;

	return true;
}

先找到两个集合的根,如果这两个不在一个集合我们就进行合并,合并的方式是将一个集合当作另一个集合的根。

当然,上面的只是逻辑图,物理模型如下:

还有一点值得注意,就是我们要让root1为较大的那个集合,root2为较小的那个集合,这样做可以提高一点效率,因为我们是将root2合并到root1上的,如果root2的元素过多,root2再接到root1下,那么将会导致,root2中的所有元素每次都要多找一个值才能找到。

3.3 集合的个数

size_t Count()
{
	size_t count = 0;
	for (auto e : _ufs)
	{
		if (e < 0)
			++count;
	}
	return count;
}

遇到一个小于0的数说明这是一个集合,每遇到一个集合直接++,最后返回最终的结果。

3.4 判断两个数是否在同一个集合

bool InSameSet(size_t x1, size_t x2)
{
	int root1 = FindRoot(x1);
	int root2 = FindRoot(x2);

	if (root1 == root2)
		return true;
	else
		return false;
}

直接找根即可,如果是同一个根就说明在同一个集合。

3.5 路径压缩

并查集的优势就是,能快速的查看两个数是否属于同一集合。但是有个问题,我们的合并两个集合是将一个集合直接变成另一个集合的孩子。也就是说,如果数据量较大的情况下,会导致并查集的高度变得很大,也就影响了查找效率,所以我们可以使用路径压缩的方式,减少树的高度。

路径压缩的方式是:我们使用FindRoot查找一个结点时,就把这个结点到根节点路径上的所有结点都变成根的孩子结点。

以下图为例:我们使用FIndRoot查找9号结点的根。

将9到根结点0路径上所有的结点都变成根节点0的孩子,也就是将9和1变成0的孩子。

我们发现以后再查找9所在的集合,只需找两次就行了。

//返回父节点的下标
size_t FindRoot(size_t posi)
{
	int root = posi;
	while (_ufs[root] >= 0)
	{
		root = _ufs[root];
	}

	//路径压缩
	while (_ufs[posi] >= 0)
	{
		int parent = _ufs[posi];
		_ufs[posi] = root;
		posi = parent;
	}

	return posi;
}

3.6 完整代码

class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	//返回父节点的下标
	size_t FindRoot(size_t posi)
	{
		int root = posi;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		//路径压缩
		while (_ufs[posi] >= 0)
		{
			int parent = _ufs[posi];
			_ufs[posi] = root;
			posi = parent;
		}

		return posi;
	}

	bool Union(size_t x1, size_t x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		// 如果他们本身就在同一个集合中
		if (root1 == root2)
			return false;

		// 默认将root1为较大的集合
		if (_ufs[root1] > _ufs[root2])
			swap(root1, root2);

		//将root2合并到root1下
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;

		return true;
	}

	size_t Count()
	{
		size_t count = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				++count;
		}
		return count;
	}

	bool InSameSet(size_t x1, size_t x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		if (root1 == root2)
			return true;
		else
			return false;
	}

private:
	vector<int> _ufs;
};

4. 并查集相关题目

4.1 省份数量

题目链接:LCR 116. 省份数量 - 力扣(LeetCode)

题目描述:

解题思路:如果两个城市连接了,就将他们加入同一个集合,最终判断有几个集合即可。

方法一:使用我们自己写的并查集

class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	//返回父节点的下标
	size_t FindRoot(size_t posi)
	{
		int root = posi;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		//路径压缩
		while (_ufs[posi] >= 0)
		{
			int parent = _ufs[posi];
			_ufs[posi] = root;
			posi = parent;
		}

		return posi;
	}

	bool Union(size_t x1, size_t x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		// 如果他们本身就在同一个集合中
		if (root1 == root2)
			return false;

		//将root2合并到root1下
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;

		return true;
	}

	size_t Count()
	{
		size_t count = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				++count;
		}
		return count;
	}

	bool InSameSet(size_t x1, size_t x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		if (root1 == root2)
			return true;
		else
			return false;
	}

    void Print()
    {
        for (auto e : _ufs)
        {
            std::cout << e << " ";
        }
        std::cout << std::endl;
    }

private:
	vector<int> _ufs;
};


class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int n = isConnected.size();
        UnionFindSet ufs(n);

        for (size_t i = 0; i < n; i++)
        {
            for (size_t j = 0; j < n; j++)
            {
                if (isConnected[i][j] == 1)
                {
                    ufs.Union(i, j);
                }
            }
        }
       ufs.Print();

        return ufs.Count();
    }
};

发现相同省份就将他们加入同一个集合即可。

但是这种方式非常麻烦,要先手动实现一个并查集,实际上我们可以发现,我们只用到了Union函数和Count函数。

方案二:模拟并查集

我们完全可以用数组模拟出一个并查集,只需要实现其中的合并功能就行了。

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int n = isConnected.size();
        vector<int> ufs(n, -1);

        auto FindRoot = [&ufs](size_t index)
        {
            int root = index;
            while (ufs[root] >= 0)
            {
                root = ufs[root];
            }
            return root;
        };

        for (size_t i = 0; i  < n; i++)
        {
            for (size_t j = 0; j < n; j++)
            {
                if (isConnected[i][j] == 1)
                {
                    //先找根
                    int root1 = FindRoot(i);
                    int root2 = FindRoot(j);

                    if (root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }
                }
            }
        }

        size_t count = 0;
        for (auto e : ufs)
        {
            if (e < 0)
                ++count;
        }
        
        return count;
    }
};

4.2 等式方程的可满足性

题目链接:990. 等式方程的可满足性 - 力扣(LeetCode)

题目描述:

解题思路:如果表达式的符号为==,就将左右两个操作数加入同一个集合中,遍历一遍之后,再次遍历,找到表达式为 != 的符号,判断左右两边是否在同一个集合中,如果不满足条件则返回false,满足则返回true。

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        int n = equations.size();
        vector<int> ufs(26, -1);

        auto FindRoot = [&ufs](size_t index)
        {
            while (ufs[index] >= 0)
            {
                index = ufs[index];
            }
            return index;
        };

        for (auto& str : equations)
        {
            if (str[1] == '=')
            {
                int root1 = FindRoot(str[0] - 'a');
                int root2 = FindRoot(str[3] - 'a');

                if (root1 != root2)
                {
                    ufs[root2] = root1;
                }
            }
        }

        for (auto& str : equations)
        {
            if (str[1] == '!')
            {
                int root1 = FindRoot(str[0] - 'a');
                int root2 = FindRoot(str[3] - 'a');

                if (root1 == root2)
                    return false;
            }
        }

        return true;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1924780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot+vue 开发记录(九)后端打包部署运行

本篇文章主要内容是后端项目写好了&#xff0c;怎么打包部署到服务器上运行。 文章目录 1. 在服务器上安装Docker2. 在Docker中装MySQL3. 在Docker中设置网桥&#xff0c;实现容器间的网络通信4. 修改后端配置文件5. 修改pom.xml文件6. 打包7. 编写DockerFile文件8. 上传文件到…

STFT:解决音频-视频零样本学习 (ZSL) 中的挑战

传统的监督学习方法需要大量的标记训练实例来进行训练,视听零样本学习的任务是利用音频和视频模态对对象或场景进行分类&#xff0c;即使在没有可用标记数据的情况下。为了解决传统监督方法的限制&#xff0c;提出了广义零样本学习&#xff08;Generalized Zero-Shot Learning,…

暴雨让服务器不怕热҈热҈热҈热҈

在AI算力呈几何倍数增长的趋势下&#xff0c;算力逐渐朝着“高性能、高密度、高耗能“发展。在高耗能的算力下&#xff0c;AI服务器功率已逐步逼近风冷散热极限&#xff0c;而液冷作为更加高效、低能耗的制冷技术&#xff0c;逐渐成为了高密度算力散热场景的首选方案。 液冷的…

Spring源码中的模板方法模式

1. 什么是模板方法模式 模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为设计模式&#xff0c;它在操作中定义算法的框架&#xff0c;将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。 模板方法模式的定义&…

Linux内核编译安装 - Deepin,Debian系

为什么要自己编译内核 优点 定制化&#xff1a;你可以根据自己的硬件和需求配置内核&#xff0c;去掉不必要的模块&#xff0c;优化性能。性能优化&#xff1a;移除不需要的驱动程序和特性&#xff0c;减小内核体积&#xff0c;提高系统性能。最新特性和修复&#xff1a;获取…

网络(二)——套接字编程

文章目录 理解源IP地址和目的IP地址认识端口号认识TCP/UDP协议网络字节序socket编程接口socket 常见APIsockaddr结构 理解源IP地址和目的IP地址 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址&#xff1b; 源IP即发送方的地址&#xff0c;目的IP即接受方的…

[译] Rust标准库有些特殊,让我们改它

本篇是对 RustConf 2023中的The standard library is special. Let’s change that.这一视频的翻译与整理, 过程中为符合中文惯用表达有适当删改, 版权归原作者所有. 今天我将讨论Rust的标准库,更具体地说,是关于标准库有何特殊之处,以及为什么我们应该改变这一点。首先声明一下…

探索 Prompt 的世界:让你的 AI 更智能

探索 Prompt 的世界&#xff1a;让你的 AI 更智能 引言什么是 Prompt&#xff1f;Prompt 的重要性如何编写有效的 Prompt1. 清晰明确2. 包含关键细节3. 提供上下文 实践中的 Prompt 技巧1. 多次迭代2. 实验不同风格3. 结合实际应用 总结 引言 随着人工智能&#xff08;AI&…

通过vm可以访问那些属性——06

1.通过vue实例都可以访问那些属性&#xff1f;&#xff08;通过vm都可以vm.什么&#xff09; vue实例中的属性很多。有的以$开始&#xff0c;有的以_开始。 所有以$开始的属性&#xff0c;可以看做是公开的属性&#xff0c;这些属性是提供给程序员使用的 所有以_开始的属性&…

PyTorch是使用GPU和CPU优化的深度学习张量库——torchvision

torchvision datasets torchvision.datasets 包含了许多标准数据集的加载器。例如&#xff0c;CIFAR10 和 ImageFolder 是其中两个非常常用的类。 CIFAR10 CIFAR10 数据集是一个广泛使用的数据集&#xff0c;包含10类彩色图像&#xff0c;每类有6000张图像&#xff08;5000张…

<数据集>夜间车辆识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;5000张 标注数量(xml文件个数)&#xff1a;5000 标注数量(txt文件个数)&#xff1a;5000 标注类别数&#xff1a;8 标注类别名称&#xff1a;[car, pedestrian, traffic light, traffic sign, bicycle, bus, truck…

Leetcode(经典题)day2

H指数 274. H 指数 - 力扣&#xff08;LeetCode&#xff09; 先对数组排序&#xff0c;然后从大的一头开始遍历&#xff0c;只要数组当前的数比现在的h指数大就给h指数1&#xff0c;直到数组当前的数比现在的h指数小的时候结束&#xff0c;这时h的值就是要返回的结果。 排序…

Ubuntu搭建Android架构so库交叉编译环境

目录 前言一、下载NDK并安装二、安装NDK三、配置交叉编译工具链四、编写交叉编译脚本 前言 需要将一些源码编译成Android可用的架构的so库 一、下载NDK并安装 https://developer.android.google.cn/ndk/downloads/ 二、安装NDK 将下载下来的android-ndk-r23b-linux.zip解压…

17099 周工作计划安排

这个问题可以通过动态规划来解决。我们可以定义一个数组d&#xff0c;其中d[i]表示第i周选择项目后&#xff0c;产生的最大效益和。然后我们可以通过比较选择低压项目和高压项目的效益&#xff0c;来更新d[i]。 以下是解题步骤&#xff1a; 1. 初始化数组&#xff1a;首先&am…

《Linux系统编程篇》认识在linux上的文件 ——基础篇

前言 Linux系统编程的文件操作如同掌握了一把魔法钥匙&#xff0c;打开了无尽可能性的大门。在这个世界中&#xff0c;你需要了解文件描述符、文件权限、文件路径等基础知识&#xff0c;就像探险家需要了解地图和指南针一样。而了解这些基础知识&#xff0c;就像学会了魔法咒语…

视频播放器的问题

<template><div class"app-container"><el-form :model"queryParam" ref"queryForm" :inline"true"><el-form-item label"题目ID&#xff1a;"><el-input v-model"queryParam.id" cle…

python:绘制一元三次函数的曲线

编写 test_x3_3x.py 如下 # -*- coding: utf-8 -*- """ 绘制函数 y x^33x4 在 -3<x<3 的曲线 """ import numpy as np from matplotlib import pyplot as plt# 用于正常显示中文标题&#xff0c;负号 plt.rcParams[font.sans-serif] […

免费的AI抠图工具 毫秒级抠图 离线可用 -鲜艺AI抠图

鲜艺AI抠图是一款免费的AI抠图工具&#xff0c;不登录、不联网&#xff0c;内嵌 AI 模型&#xff0c;快至毫秒级抠图&#xff0c;支持批量抠图&#xff0c;支持点击按钮选择图片、拖入图片、粘贴图片、粘贴图片链接、从网页拖入图片&#xff0c;支持Windows和macos&#xff0c;…

Linux:Linux网络总结(附下载链接)

文章目录 下载链接网络问题综合问题访问一个网页的全过程&#xff1f;WebSocket HTTPHTTP基本概念GET与POSTHTTP特性HTTP缓存技术HTTP的演变HTTP1.1 优化 HTTPSHTTP与HTTPS有哪些区别&#xff1f;HTTPS解决了HTTP的哪些问题&#xff1f;HTTPS如何解决的&#xff1f;HTTPS是如何…

【触想智能】安卓工控一体机在自助终端设备上的应用分析

随着科技的发展和人们对自动化系统的需求不断增强&#xff0c;自助终端设备已经成为日常生活非常常见的设备之一&#xff0c;例如自助售货机、自助点餐机、自助银行服务等。这些设备在使用中都需要一个可靠的、稳定的操作系统来支持其各项功能的实现。 因此&#xff0c;安卓工控…