并查集原理及代码实现

news2025/2/28 15:49:06

并查集

首先要明确的是并查集是森林。由多棵树组成。

并查集 (英文:Disjoint-set data structure,直译为不交集数据结构),用于处理一些 不交集 (Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。

并查集支持如下操作:1、查询:查询某个元素属于哪个集合,通常是返回集合内的一个"代表元素"。这个操作是为了判断两个元素是否在同一个集合之中。2、合并:将两个集合合并为一个。3、添加 :添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。这个数据结构同时支持查询和合并这两种操作。

问题:给了一堆类型为string的数据,应该怎么给他们编号呢?怎么建立对应的映射关系呢?

1、通过编号找数据。把这堆数据存到vector结构里,每个数据就有了自己的编号;2、通过数据找编号。把数据存到map结构中,就可以快速通过编号找到数据。

问题:如何描述数据之间的关系呢?怎么去建立这棵树?

某一部分属性相同的数据归到一个集合里。每个集合中任选一个节点去做根,这个集合中其他的节点作孩子。

题外话:与堆类似(用数组来当底层的数据结构),并查集用数组来表示多棵树,用数组下标来表示关系。用的是双亲表示法,保存父节点的下标即可。B树用的是三叉链。

原理

一个例子:某实习组招生10人,哈尔滨招4人,云南招3人,西藏招3人,10个人来自不同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数。

 我  你  他   双  时   而  先  句   吃  高 //学生姓名
 0   1   2   3   4   5   6   7   8   9 //挨个放入vector,人名映射数组编号
-1  -1  -1  -1  -1  -1  -1  -1  -1  -1 //数组--表示该小集体成员个数

数组的初始值给的是-1,表示10个数据每个数据分别是一个集合,集合内无成员。

比如“我”和“先”在一个集合,假设"我"做根,那“我”对应的数组值-1加上"先"对应的-1,得-2,"先"映射的数据编号为6,其对应的数组值改为"我"映射的数据编号0。

 我  你  他   双  时   而  先  句   吃  高 //学生姓名
 0   1   2   3   4   5   6   7   8   9 //人名映射数组编号
-2  -1  -1  -1  -1  -1   0  -1  -1  -1 //数组

接下来大家结伴去实习地。哈尔滨学生小分队s1={“我”, “先”, “句”, “吃”},云南学生小分队s2={“你”, “时”, “高”},西藏学生小分队s3={“他”, “双”, “而”}就相互认识了,10个人形成了三个小团体。假设右三个群主“我”, “你”, “他”担任队长,负责大家的出行。

根据刚才讲解的"我"和"先"组成一个集合的方法,给这10个人组成的3个集合进行分类,最后得到的情况如下

10人分成3组

 我  你  他   双  时   而  先  句   吃  高 //学生姓名
 0   1   2   3   4   5   6   7   8   9 //人名映射数组的下标
-4  -3  -3   2   1   2   0   0   0   1 //数组

从上列表可以看出:编号6, 7, 8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3个人(包含队长1)。

仔细观察数组中内融化,可以得出以下结论:

  1. 数组的下标表示学生姓名和数组的映射关系;
  2. 数组的值如果为负数,那么这个位置映射的学生就是集合的代表数据(树的根),该数字的绝对值代表该集合中元素个数
  3. 数组的值如果为非负数,这个值表示该元素的父亲在数组中的下标

实习一段时间后,哈尔滨小分队和云南小分队相互认识,最后合并为一个集合。

最终形成2个集合

 我  你  他   双  时   而  先  句   吃  高 //学生姓名
 0   1   2   3   4   5   6   7   8   9 //人名映射数组的下标
-7   0  -3   2   1   2   0   0   0   1 //数组

此时将云南小分队的队长"你"加入到"我"的集合中,只需要修改"我"和"你"对应的数组的值即可。现在"我"集合有7个人,"他"集合有3个人,总共2个集合。

功能

通过以上例子可知,沿着数组表示的树形关系可以找到根。并查集一般可以解决一下问题:

  1. 查找元素属于哪个集合:根据数组的值(即根据该值表示的是树形结构里的父亲节点下标)一直找到根(当数组值为负数就是根);
  2. 查看两个元素是否属于同一个集合:根据数组的值一直找到树的根,如果根相同表明在同一个集合,否则不在同一个集合;
  3. 将两个集合归并成一个集合:a.将两个集合中的元素合并;b.将一个集合名称改成另一个集合的名称;
  4. 集合的个数:遍历数组,数组中元素为负数的个数即为集合的个数。

优化

路径压缩

我们在合并集合的时候,是将根节点编号大的树直接当作另一颗根节点编号小的树的子树(合并的这个集合层数就会越来越多)。当合并的集合多了的话,我们在找根的时候,每次找根都要一层一层地往上找,效率会低。

此时的优化方法就是在查找根的时候,进行压缩。举例:查找x的时候,发现找到根的时候x在数组里的值(现在的父亲不等于根),就把x在数组中对应的值改成最终找到的根的下标。

// 优化2:路径压缩
int FindRoot(int index)
{
    // 树形结构 存储的是父节点的下标
    int root = index;
    // 如果当前下标对应的值>=0,说明她们不是根,要继续查找
    while (_ufs[root] >= 0) root = _ufs[root];

    // 路径压缩
    while (_ufs[index] >= 0)
    {
        int parent = _ufs[index];
        // 走过的每个节点都成为根的孩子
        _ufs[index] = root;
        index = parent;
    }

    return root;
}
// 合并元素 -- 合并原则:按根节点下标的大小
bool Union(int x1, int x2)
{
    int root1 = FindRoot(x1);//找到下标为x1的根节点
    int root2 = FindRoot(x2);//找到下标为x2的根节点

    // x1已经与x2在同一个集合(根节点一样,说明在同一个集合)
    if (root1 == root2)
        return false;

    // 把下标大的根节点往下标小的根节点集合去合并
    if (root1 > root2)
        swap(root1, root2);

    // 将两个集合中元素合并
    _ufs[root1] += _ufs[root2];

    // 将其中一个集合名称改变成另外一个
    _ufs[root2] = root1;
    return true;
}

启发式压缩:优化合并原则

启发式压缩就是边合并的时候边优化。将合并原则修改为:按所在集合元素多少来合并,把数据少的小集合合并到数据多的大集合去

// 优化1:优化合并原则
int FindRoot(int index)
{
    //树形结构 存储的是父节点的下标
    int parent = index;
    //如果当前下标对应的值>=0,说明她们不是根,要继续查找
    while (_ufs[parent] >= 0) parent = _ufs[parent];
    return parent;
}
//合并元素 合并原则:按所在集合元素多少
bool Union(int x1, int x2)
{
    int root1 = FindRoot(x1);//找到下标为x1的根节点
    int root2 = FindRoot(x2);//找到下标为x2的根节点

    // x1已经与x2在同一个集合(根节点一样,说明在同一个集合)
    if (root1 == root2)
        return false;

    //把数据量小的往大集合去合并
    if(abs(_ufs[root1]) < abs(_ufs[root2]))
        swap(root1, root2);

    // 将两个集合中元素合并
    _ufs[root1] += _ufs[root2];

    // 将其中一个集合名称改变成另外一个
    _ufs[root2] = root1;
    return true;
}

当然,把这两种优化方法结合起来,优化效果更好!找根的时候就不用层层往回找了。

代码

基础版,直接使用数据的编号来操作

#include <iostream>
#include <vector>
using namespace std;

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

	// 给一个元素的编号,找到该元素所在集合的名称
	int FindRoot(int index)
	{
		//树形结构 存储的是父节点的下标
		int parent = index;
		//如果当前下标对应的值>=0,说明她们不是根,要继续查找
		while (_ufs[parent] >= 0) parent = _ufs[parent];
		return parent;
	}

	//合并元素
	bool Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);//找到下标为x1的根节点
		int root2 = FindRoot(x2);//找到下标为x2的根节点

		// x1已经与x2在同一个集合(根节点一样,说明在同一个集合)
		if (root1 == root2)
			return false;

		// 将两个集合中元素合并
		_ufs[root1] += _ufs[root2];

		// 将其中一个集合名称改变成另外一个
		_ufs[root2] = root1;
		return true;
	}

	//集合个数
	size_t Count()const
	{
		int count = 0;
		//如果当前下标对应的值<0,说明是根,就表示一个集合
		for (auto e : _ufs)
		{
			if (e < 0) count++;
		}
		return count;
	}

	//判断两个数据是否在同一个集合里
	bool InSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}
private:
	vector<int> _ufs;
};

升级版,需要自己建立数据和下标的映射关系

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;

template<class T>
class Set
{
public:
	Set(const T* a, size_t n)
	{
		for (size_t i = 0; i < n; ++i)
		{
			_a.push_back(a[i]);//给每个数据进行编号
			_indexMap[a[i]] = i;//将数据和编号建立映射关系
		}
	}
	vector<T> _a;//根据编号找数据。先将数据存到vector结构体里,每个数据就有了自己的编号
	map<T, int> _indexMap;//根据数据找编号
};

template<class T>
class UnionFindSet
{
public:
	int getIndex(const T& key)
	{
		auto it = _set._indexMap.find(key);
		if (it == _set._indexMap.end())
		{
			cout << "查无此人, 请重新输入" << endl;
			exit(1);
		}
		return it->second;
	}

	UnionFindSet(Set<T>& set)
		:_set(set), _ufs(set._a.size(), -1)
	{}

	// 给一个元素的编号,找到该元素所在集合的名称
	T FindRoot(const T& name)
	{
		int num = getIndex(name);
		while (_ufs[num] >= 0) num = _ufs[num];
		//找到根的编号了
		return _set._a[num];
	}

	// 求集合的个数,即数组中负数的个数
	size_t Count()const
	{
		size_t count = 0;
		for (auto& e : _ufs)
		{
			if (e < 0) ++count;
		}
		return count;
	}

	// 合并集合
	bool Union(const T& name1, const T& name2)
	{
		T root1 = FindRoot(name1);
		T root2 = FindRoot(name2);
		//两个数据已经在同一个集合里了
		if (root1 == root2) return false;
		//合并 把第二个集合 合并 到第一个集合里
		int num1 = getIndex(name1);
		int num2 = getIndex(name2);
		// 找到第二个集合的根
		while (_ufs[num2] >= 0) num2 = _ufs[num2];

		_ufs[num1] += _ufs[num2];
		_ufs[num2] = num1;
		return true;
	}
private:
	Set<T> _set;
	vector<int> _ufs;
};

题目

剑指 Offer II 116. 省份数量

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        vector<int> ufs(isConnected.size(), -1);
        //lambda表达式
        auto findRoot = [&ufs](int x){
            while(ufs[x] >= 0) x = ufs[x];
            return x;
        };

        for(size_t i = 0; i < isConnected.size(); ++i)
        {
            for(size_t j = 0; j < isConnected[i].size(); ++j)
            {
                if(isConnected[i][j] == 1)//表示城市有连接,可以进入一个集合
                {
                    int root1 = findRoot(i);
                    int root2 = findRoot(j);
                    if(root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }                    
                }
            }
        }
        int count = 0;
        for(auto e: ufs)
        {
            if(e < 0) count++;
        }
        return count;
    }
};

990. 等式方程的可满足性

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        //相等的值 就在一个集合中,不相等的值不能在 t
        vector<int> ufs(26, -1);//26个字母

        auto findRoot = [&ufs](int x){
            while(ufs[x] >= 0) x = ufs[x];
            return x;
        };

        // 第一遍,先把相等的值加到一个集合中
        for(auto& str : equations)
        {
            if(str[1] == '=')
            {
                int root1 = findRoot(str[0] - 'a');
                int root2 = findRoot(str[3] - 'a');
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }
        // 第二遍,先看不相等的值在不在一个集合,如果在,就返回false
        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/435822.html

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

相关文章

android framework-SystemServer进程

SystemServer进程信息 一、SystemServer整体时序图 涉及源码路径&#xff1a; android-10.0.0_r41\frameworks\base\core\java\com\android\internal\os\ZygoteInit.java android-10.0.0_r41\frameworks\base\core\java\com\android\internal\os\Zygote.java android-10.0.0_r4…

JAVAWeb07-WEB 开发通信协议-HTTP 协议-关联篇

1. 概述 1.1 官方文档及示例说明 1.1.1 请求头 1.1.2 响应头 1.1.3 HTTP 响应状态码 HTTP状态码 当浏览者访问一个网页时&#xff0c;浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前&#xff0c;此网页所在的服务器会返回一个包含HTTP状态码的信息头&a…

leetcode 1372. Longest ZigZag Path in a Binary Tree(二叉树中最长的之字形路径)

找出最长的之字型路径长度。 可以选择从二叉树的任意一个节点出发。 路径长度为路径中的节点数-1. 思路&#xff1a; 符合DFS的特征。 方向是左右交替的&#xff0c;可以定义0&#xff0c;1两个方向。 如果当前方向是左&#xff0c;下一方向就是右&#xff0c;反之亦然。每次…

FinClip|小程序云开发的那点事儿

在开发一个小程序时&#xff0c;除了考虑界面功能逻辑外&#xff0c;还需要后端的数据支持&#xff0c;开发者需要提前考虑服务器、存储和数据库等相关需求的支持能力&#xff0c;此外还可能需要花费时间精力在部署应用、和依赖服务的建设上。 因此&#xff0c;微信小程序为了…

dolphinscheduler3.1.3版本代码编译运行方法

说明 该文档适用于dolphinscheduler 3.1.3-release版本。 一 环境准备 需要使用的环境包括JDK1.8&#xff0c;以及Maven 3.6以上的版本&#xff0c;这里使用低于3.6版本的Maven也可以调试运行&#xff0c;不过在打包的时候会有报错&#xff0c;最好使用高版本的maven。 二 …

数字IC笔试面试常考问题及答案汇总(内含各岗位大厂题目)

经历了无数的笔试面试之后&#xff0c;不知道大家有没有发现数字IC的笔试面试还是有很多共通之处和规律可循的。所以一定要掌握笔试面试常考的问题。 数字IC笔试面试常考问题及答案汇总&#xff08;文末可领全部哦~&#xff09; 验证方向&#xff08;部分题目&#xff09; Q1…

android studio APP工程的项目结构说明及创建

目录 1.APP工程的项目结构图 2.功能说明 2.1app 2.2Gradle Scripts 3.创建新的APP页面 补充&#xff1a; 1.APP工程的项目结构图 2.功能说明 该项目下两个分类&#xff1a;一个是app&#xff08;代表app模块&#xff09;;另一个是Gradle Scripts。 2.1app app下面有3个…

虚拟机安装Centos7,ping不通百度

虚拟机安装Centos7&#xff0c;ping不通百度 一、虚拟机网络配置 网络适配器选择桥接模式&#xff0c;不勾选复制物理网络连接状态。 同时虚拟机使用默认配置都是桥接。 二、配置静态IP 1、首先&#xff0c;查看宿主机的IP和网关 2、配置静态ip的文件地址及修改命令如下&…

mybatis分页插件的基本理解和使用

mybatis分页插件的基本理解和使用 为什么要使用mybatis分页插件&#xff1f; 分页是一种将所有数据分段展示给用户的技术。用户每次看到的不是全部数据&#xff0c;而是其中一部分&#xff0c;如果在其中没有找到自己想要的内容&#xff0c;用户可以通过制定页码或者是翻页的…

就业并想要长期发展选数字后端还是ic验证?

“就业并想要长期发展选数字后端还是ic验证&#xff1f;” 这是知乎上的一个热点问题&#xff0c;浏览量达到了13,183。看来有不少同学对这个问题感到疑惑。之前更新了数字后端&数字验证的诸多文章&#xff0c;从学习到职业发展&#xff0c;都写过&#xff0c;唯一没有做过…

CRM系统能帮助企业解决哪些问题?

随着信息化技术的不断发展和全球化的推进&#xff0c;市场竞争越来越激烈&#xff0c;客户需求也在不断变化。为了应对这种情况&#xff0c;越来越多的企业开始使用CRM系统来管理与客户的关系。那么&#xff0c;CRM系统到底解决了企业哪些问题呢&#xff1f; 一、提高客户满意…

亿发软件:传统食品饮料批发行业如何通过信息化管理系统降本增效?

传统食品饮料批发行业信息化水平较低&#xff0c;存在多重管理难题&#xff0c;例如&#xff1a; 手动数据输入和管理&#xff0c;导致错误和效率低下&#xff1b; 数据缺乏实时可见性&#xff0c;无法实时了解企业仓库存量、销售额和其他关键业务指标&#xff1b; 低效的供应链…

【Android】之【常用布局】

一、简介 Android常用布局分别是 1、线性布局LinearLayout 2、相对布局RelativeLayout 3、绝对布局AbsoluteLayout 4、帧布局FrameLayout 5、表格布局TableLayout 6、网格布局GridLayout 7、约束布局ConstraintLayout 二、详解 2.1. LinearLayout (线性布局) 线性布局是一种非…

Rabbit与springboot整合-1

目录 1、整体结构 2、pom引入 3、配置文件 4、代码 公共类 controller类 JSON转换类 监听-接收发送消息类 1、整体结构 2、pom引入 <!--rabbitmq--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-st…

C++引用与引用两大应用场景,临时变量的常性,常引用与权限大小问题

tips 内存栈区的用习惯是先使用高地址&#xff0c;然后使用低地址嘛顺序表数组支持随机下标访问&#xff0c;也是目前已知的仅有的数据结构类当中的话&#xff0c;它不可以不仅可以去定义变量&#xff0c;它也可以定义函数&#xff0c;这个跟c当中的结构体不一样的&#xff0c…

数据结构考研版——队列的配置问题

一、正常配置下的情况 队空状态 frontrear;入队操作 出队操作 队满状态 在正常配置下元素的个数&#xff08;rear>front&#xff09; 当rear<front 综上所述用一个表达式表示&#xff1a;(rear-frontmaxSize)%maxSize 二、非正常配置下的情况1 队空状态 入队操作…

Stable Diffusion-webUI ckpt模型、lora模型的区别和使用

一、常用的两种模型&#xff1a;ckpt和Lora分别是什么&#xff1f;有什么区别&#xff1f; 1、CKPT&#xff08;CheckPoint&#xff09; 经过训练的图片合集&#xff0c;被称作模型&#xff0c;也就是chekpoint&#xff0c;体积较大&#xff0c;一般真人版的单个模型的大小在…

StarRC的妙用

在整个R2G的流程里边&#xff0c;寄生参数抽取&#xff08;StarRC&#xff09;是比较没有存在感的。大部分的时间&#xff0c;工程师们只是用这个工具来刷SPEF。并不会关注太多。这本身其实是一个好事情&#xff0c;反向证明了参数抽取工具的高度稳定性&#xff01; 但是&#…

虚拟数字人的3种驱动方式

虚拟数字人是由计算机程序所构建的具有人类特征的虚拟实体&#xff0c;目前的虚拟数字人经过了三代的更迭&#xff0c;划分每一代更迭的标准则是虚拟数字人的驱动方式。 一、虚拟数字人1.0&#xff1a;动画&CG驱动 虚拟数字人1.0就是目前我们所熟知的&#xff0c;比如&am…

[NOIP2000 提高组] 进制转换

[NOIP2000 提高组] 进制转换 题目描述 我们可以用这样的方式来表示一个十进制数: 将每个阿拉伯数字乘以一个以该数字所处位置为指数,以 10为底数的幂之和的形式。例如 123 可表示为 10^22*10^13*10^0 这样的形式。 与之相似的&#xff0c;对二进制数来说&#xff0c;也可表示成…