【数据结构与算法】并查集

简介: 【数据结构与算法】并查集

👉并查集的原理👈


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


并查集需要建立映射关系,那么下面的代码是建立映射关系的一种方法(并查集的实现不采用这种方法)。


template <class T>
class UnionFindSet
{
public:
  UnionFindSet(const T* a, size_t n)
  {
    for (size_t i = 0; i < n; ++i)
    {
      // 建立映射关系
      _a.push_back(a[i]);
      _indexMap[a[i]] = i;
    }
  }
private:
  vector<T> _a; // 编号找人
  map<T, int> _indexMap;  // 人找编号
};

29de5a6b066e4de3955c973b73b2ae87.png

比如:某公司今年校招全国总共招生 10 人,西安招 4 人,成都招 3 人,武汉招 3 人,10 个人来自不同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数。(负号下文解释,初始状态给的是 -1,表示 10 棵树,10 个集合)


7b2323cbf36c45d89d933646251cc81e.png


毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起上路。西安学生小分队 s1 = {0,6,7,8},成都学生小分队 s2 = {1,4,9},武汉学生小分队 s3 = {2,3,5} 就相互认识了,10 个人形成了三个小团体。假设有三个群主0,1,2 担任队长,负责大家的出行。

3a08e5fc356b4dfe9df2390d0d9f5e3e.png



一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。


9e635e5b6840427aa41529ea41c345ac.png


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



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

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

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

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

4. 并查集的表示方法与堆类似,用下标表示关系,采用的是双亲表示法(存储父亲的下标)



在公司工作一段时间后,西安小分队中 8 号同学与成都小分队 1 号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子。


a49e00c61e0d4bd6b7c155757de83321.png


现在 0 集合有 7 个人,2 集合有 3 个人,总共两个朋友圈。



通过以上例子可知,并查集一般可以解决一下问题:


查找元素属于哪个集合

沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)

查看两个元素是否属于同一个集合

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

将两个集合归并成一个集合

将两个集合中的元素合并

将一个集合名称改成另一个集合的名称

集合的个数

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


👉并查集的实现👈


#pragma once
#include <map>
#include <vector>
#include <iostream>
#include <string>
using namespace std;
class UnionFindSet
{
public:
  // 初始状态
  UnionFindSet(size_t n)
    : _ufs(n, -1)
  {}
  // 将x1和x2所在的集合合并
  void Union(int x1, int x2)
  {
    int root1 = FindRoot(x1);
    int root2 = FindRoot(x2);
    // x1和x2已经在同一个集合了
    if (root1 == root2)
      return;
    // x1和x2不在同一个集合,需要合并两个集合
    // 假设下标小的去做根
    if (root1 > root2)
      swap(root1, root2);
    _ufs[root1] += _ufs[root2];
    _ufs[root2] = root1;
  }
  // 判断x1和x2是否在同一个集合中
  bool Inset(int x1, int x2)
  {
    return FindRoot(x1) == FindRoot(x2);
  }
  // 集合(树)的个数 = 负数的个数
  size_t SetSize()
  {
    size_t size = 0;
    for (size_t i = 0; i < _ufs.size(); ++i)
    {
      if (_ufs[i] < 0)
        ++size;
    }
    return size;
  }
private:
  vector<int> _ufs;
  // 找出x的根
  int FindRoot(int x)
  {
    // 值为负数时即为根
    while (_ufs[x] >= 0)
    {
      x = _ufs[x];
    }
    return x;
  }
};


👉并查集的应用👈


并查集的主要应用就是求取朋友圈的数量,即是否在同一个集合中等问题。


省份数量


有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份是一组直接或间接相连的城市,组内不含其他没有相连的城市。


给你一个 n x n 的矩阵 isConnected ,其中isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。


返回矩阵中省份的数量。

22698af52642479f86500a535a8bc3a2.png


思路:先构造一个并查集,当 isConnected[i][j] = 1 时,说明第 i 个城市和第 j 个城市直接相连,将它们放在同一个集合;isConnected[i][j] = 0 表示二者不直接相连,不在同一个集合中。遍历结束,并查集中集合的个数就是省份的数量。


// 注:需要将并查集的代码拷贝到最前面
class Solution 
{
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        size_t n = isConnected.size();
        UnionFindSet ufs(n);
        for(size_t i = 0; i < n; ++i)
        {
            for(size_t j = i + 1; j < n; ++j)
            {
                if(isConnected[i][j] == 1)
                    ufs.Union(i, j);
            }
        }
        return ufs.SetSize();
    }
};

955fff0d427d4033b2a5451fc2eb3c6c.png


因为我们实现了并查集,所以我们做起来就会很简单。但是如果我们没有实现并查集,我们也可以通过一个数组来模拟实现并查集最核心的接口。


class Solution 
{
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        size_t n = isConnected.size();
        vector<int> ufs(n, -1);
        // lambda表达式:找x的根
        auto FindRoot = [&ufs](int x)
        {
            while(ufs[x] >= 0)
            {
                x = ufs[x];
            }
            return x;
        };
        for(size_t i = 0; i < n; ++i)
        {
            for(size_t j = i + 1; 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(size_t i = 0; i < n; ++i)
        {
            if(ufs[i] < 0)
                ++count;
        }
        return count;
    }
};

1cea36629d5b4c5ba07108196cfdaa36.png


等式方程的可满足性


给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

de1b4afa249745f9a613b522d2bf1fac.png


本道题也是可以通过并查集来解决的。因为 equations[i][0] 和 equations[i][3] 是小写字母,所以我们申请一个能存储 26 个元素的数组(相对映射)。当 equations[i][1] 为 ‘=’ 时,说明两个小写字母在同一个集合中;当 equations[i][1] 为 ‘!’ 时,说明两个小写字母不在同一个集合中。第一次遍历,将相等的小写字母放在同一个集合中;第二次遍历,看不在同一个集合的小写字母是否在同一个集合中。如果是,说明等式方程不满足,返回 false;如果第二次遍历结束,说明等式方程满足,返回 true。


class Solution 
{
public:
    bool equationsPossible(vector<string>& equations) 
    {
        vector<int> ufs(26, -1);
        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;
                }
            }
        }
        // 第二次遍历,看不在同一个集合的小写字母是否在同一个集合中
        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;    // 等式方程满足
    }
};

1c18c84168f847179bc8b35c42308da9.png


👉并查集的优化👈


并查集的优化方式就是路径压缩和将节点少的集合向节点多的集合合并。当层数过多时,会影响查找的效率。那如何进行路径压缩呢?路径压缩可以在查找根的过程中进行,路径压缩就是修改当前下标的父节点的下标。为什么要将节点少的集合向节点多的集合合并呢?因为这样可以让更少的节点的层数变高,提高查找效率。


#pragma once
#include <map>
#include <vector>
#include <iostream>
#include <string>
using namespace std;
class UnionFindSet
{
public:
  // 初始状态
  UnionFindSet(size_t n)
    : _ufs(n, -1)
  {}
  // 将x1和x2所在的集合合并
  void Union(int x1, int x2)
  {
    int root1 = FindRoot(x1);
    int root2 = FindRoot(x2);
    // x1和x2已经在同一个集合了
    if (root1 == root2)
      return;
    // x1和x2不在同一个集合,需要合并两个集合
    // 默认root1是节点多的集合
    // 将节点少的集合合并到节点多的集合中
    if (abs(_ufs[root1]) < abs(_ufs[root2]))
      swap(root1, root2);
    _ufs[root1] += _ufs[root2];
    _ufs[root2] = root1;
  }
  // 判断x1和x2是否在同一个集合中
  bool Inset(int x1, int x2)
  {
    return FindRoot(x1) == FindRoot(x2);
  }
  // 集合(树)的个数 = 负数的个数
  size_t SetSize()
  {
    size_t size = 0;
    for (size_t i = 0; i < _ufs.size(); ++i)
    {
      if (_ufs[i] < 0)
        ++size;
    }
    return size;
  }
  // 找出x的根
  int FindRoot(int x)
  {
    int root = x;
    // 值为负数时即为根
    while (_ufs[root] >= 0)
    {
      root = _ufs[root];
    }
    // 压缩路径
    while (_ufs[x] >= 0)
    {
      // 修改该路径中节点的父亲节点下标
      int parent = _ufs[x];
      _ufs[x] = root;
      x = parent;
    }
    return root;
  }
private:
  vector<int> _ufs;
};
void UnionFindSetTest()
{
  UnionFindSet ufs(10);
  ufs.Union(8, 9);
  ufs.Union(7, 8);
  ufs.Union(6, 7);
  ufs.Union(5, 6);
  ufs.Union(4, 5);
  ufs.FindRoot(9);
}


👉总结👈


本篇博客主要讲解了什么是并查集、并查集的模拟实现、并查集的应用:省份数量和等式方程的可满足性以及并查集的优化等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

相关文章
|
2月前
|
算法 开发者 计算机视觉
燃爆全场!Python并查集:数据结构界的网红,让你的代码炫酷无比!
在编程的世界里,总有一些数据结构以其独特的魅力和高效的性能脱颖而出,成为众多开发者追捧的“网红”。今天,我们要介绍的这位明星,就是Python中的并查集(Union-Find)——它不仅在解决特定问题上大放异彩,更以其优雅的设计和强大的功能,让你的代码炫酷无比,燃爆全场!
41 0
|
3月前
|
算法 JavaScript 前端开发
第一个算法项目 | JS实现并查集迷宫算法Demo学习
本文是关于使用JavaScript实现并查集迷宫算法的中国象棋demo的学习记录,包括项目运行方法、知识点梳理、代码赏析以及相关CSS样式表文件的介绍。
第一个算法项目 | JS实现并查集迷宫算法Demo学习
|
2月前
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
37 1
|
3月前
|
Python
逆天改命!掌握Python并查集,数据结构难题从此不再是你的痛!
在编程旅程中,遇到棘手的数据结构难题是否让你苦恼?别担心,Python并查集(Union-Find)是你的得力助手。这是一种高效处理不相交集合合并及查询的数据结构,广泛应用于网络连通性、社交网络圈子划分等场景。通过维护每个集合的根节点,它实现了快速合并与查询。本文将介绍并查集的基本概念、应用场景以及如何在Python中轻松实现并查集,帮助你轻松应对各种数据结构挑战。
40 3
|
3月前
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
在编程与算法的广袤天地中,总有一些工具如同神兵利器,能够助你一臂之力,在复杂的问题前游刃有余。今天,我们就来深入探讨这样一件神器——Python并查集(Union-Find),看看它是如何让你在算法界呼风唤雨,轻松应对各种复杂场景的。
82 2
|
3月前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
3月前
|
Python
告别低效!Python并查集:数据结构界的超级英雄,拯救你的编程人生!
告别低效!Python并查集:数据结构界的超级英雄,拯救你的编程人生!
34 0
|
3月前
|
算法 开发者 计算机视觉
Python并查集:数据结构界的肌肉男,让你在编程路上无所畏惧!
在编程的浩瀚宇宙中,数据结构如同基石,构建了解决问题的坚实框架。而并查集(Union-Find),这位数据结构界的“肌肉男”,以其独特的魅力和强大的功能,让无数开发者在面对复杂关系处理时,都能感受到前所未有的从容与自信。今天,就让我们一同揭开并查集的神秘面纱,看看它是如何成为你编程路上的得力助手的。
37 0
|
3月前
|
算法 程序员 计算机视觉
Python并查集:数据结构界的肌肉男,让你在编程路上无所畏惧!
并查集,一种处理不相交集合合并与查询的数据结构,被誉为编程的“肌肉男”。它提供Find(找根节点)和Union(合并集合)操作,常用于好友关系判断、图像处理、集合合并等。Python实现中,路径压缩和按秩合并优化效率。并查集的高效性能使其成为解决问题的强大工具,助力程序员应对复杂挑战。
35 0
|
5月前
|
算法 程序员 图形学
脑洞大开!Python并查集:用最简单的方式,解决最复杂的数据结构问题!
【7月更文挑战第17天】并查集,数据结构明星,处理不相交集合合并与查询。Python实现核心操作:查找与合并。路径压缩优化查找,按秩合并保持平衡。实战应用如图连通性判断,算法竞赛利器。掌握并查集,解锁复杂问题简单解法,照亮编程之旅!
64 10