【C++】并查集模拟实现

简介: 并查集模拟实现

一. 什么是并查集?

并查集是一种表示元素与集合之间关系的表示型数据结构。其功能主要有两个:


并:合并两个集合。

查:查找两个元素是否在同一个集合。

下面我们假设一个情景:R公司在A高校中招收了10名实习生,一开始这些人都互不认识。我们用一个数组来表示他们之间的关系,其中下标为[0, 9]代表每一位同学的编号,因为他们都互不认识,所以每一个都各自代表一个集合初始时一共有10个集合,他们的值一开始都存-1。

c8a25fdbf4df44f387367ac8f43ae7c1.png

其中下标编号为0、1、2、3的四位同学都来自西安;下标为4、5、6的三位同学来自于云南;下标为7、8、9的三位同学来自北京。公司考虑把来自相同地方的同学编为一组,这样他们就相互认识了。


b609e59adf15419d8900e250639cfc49.png

在物理上我们使用数组来表示这几个同学之间的关系:每个下标存的是该位置直接双亲的下标,根下标存的是一个负值,其绝对值代表该集合的元素个数。


3bff468cad504ac98d073df60e00652d.png

实习过程中来自云南的同学4认识了来自北京的同学8,交流之后才知道他们原来是同一个学校的。

e51daa2c72614814bd319b22772b09eb.png


就这样,来自云南和北京的两个集合(六个人)相互认识了,他们变为一个集合,这就是并查集中的“并”功能。

69086fedca464910953d051f70ac1357.png


并查集还有一个功能就是查找两个元素是否在一个集合:

f015a11a80924e18a72ed0297ecbdbd8.png


二. 如何实现并查集?      

通过上面例子我们发现:在逻辑上我们用树表示一个集合,物理上用数组(并查集)来表示一个个元素和所有集合之间的关系。这个数组是个整形数组,下标对应一个个元素,而下标的值存的是它直接父亲的下标、根下标存的是该集合元素总数的负值。


假设给定元素的类型是名字,不是下标编号了,这时该怎么办?创建一个元素类型的数组,让每个元素映射它的下标编号,这样就可以用下标编号代表元素了。

fe6e64df3fdc493a9f8757d7c983a843.png



1. 并查集基本框架

成员变量是个整形数组_ufs。构造函数传入数组的长度,通过resize()初始化_ufs令它的值一开始全为-1。


class UnionFindSet
{
  public:
    UnionFindSet(int n)
    {
      _ufs.resize(n, -1);
    }
  private:
    vector<int> _ufs;
};


2. 成员函数介绍

2.1 接口一:查找元素的根下标

功能:传入当前元素的下标,返回该元素的根下标。


思路:通过下标逐个地向上的访问,直到值为负数拿到的该下标就是要找的根下标。

int FindRoot(int index)
{
  while(_ufs[index] >= 0)
  {
    index = _ufs[index];
  }
  return index;
}


2.2 接口二:合并两个元素所在的集合

功能:传入两个元素的下标x1和x2,合并这个元素所在的集合。


思路:先查找的x1和x2的根下标root1、root2,判断它们如果相同说明已经在一个集合中,不用再处理了;如果不相同,合并root1和root2代表的集合。


_ufs[root1] += _ufs[root2]。更新root1下标的值,即合并后的元素个数。

_ufs[root2] = root1。更新root2下标的值为它直接父亲节点的下标root1。

// 查找根下标
int FindRoot(int index)
{
   while(_ufs[index] >= 0)
   {
     index = _ufs[index];
   }
   return index;
 }
// 合并两元素所在的个集合
void Union(int x1, int x2)
{                                                                                                                 
  int root1 = FindRoot(x1);
  int root2 = FindRoot(x2);
  if(root1 != root2)       
  {                    
    _ufs[root1] += _ufs[root2];
    _ufs[root2] = root1;       
  }                     
}


2.3 接口三:获取集合个数

遍历一遍数组,如果值为负数就代表是一个根下标,也就是一个集合,统计根下标的数量即可。


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


3. 并查集的完整实现代码

class UnionFindSet                     
{                               
  public:  
    UnionFindSet(int n)
    {
      _ufs.resize(n, -1);
    }    
    int FindRoot(int index) 
    {      
      while(_ufs[index] >= 0)
      {      
        index = _ufs[index];
      }      
      return index;
    }                  
    void Union(int x1, int x2)
    {         
      int root1 = FindRoot(x1);
      int root2 = FindRoot(x2);
      if(root1 != root2)
      {
        _ufs[root1] += _ufs[root2];
        _ufs[root2] = root1;
      }
    }
    int GetSet()
    {
      int count = 0;
      for(auto e : _ufs)
      {
        if(e < 0)
        {
          ++count;
        }
      }
      return count;
     }                                                                                        
  private:
    vector<int> _ufs;
};


三. 为什么要有并查集?

1. 并查集的用途

现实生活中,有两种类型的问题可以借助并查集来解决:


用途1:用来确定无向图中连通子图(集合)的个数。

用途2:用来解决最小生成树的问题。

2. 并查集相关题目总结

下面是一些可以使用并查集来解决的OJ题目


第一题:等式方程的可满足性

题目连接


题目描述:


5686d762defe42069f14d017f6817f99.png0a730eff41c945b7be01c966cb4e5105.png



解题思路:

首先对于每一个小写字母,我们可以通过alpha - ‘a’映射得到对应字母alpha的下标编号。然后分类对每一个关系字符方程进行处理:


先处理“==”的方程,把符号两边的字符统一到一个集合里。

再处理“!=”的方程,反证“!=”符号两边的字符如果在一个集合中就返回false。如果都顺利处理完了就返回true。

完整代码:


// 这里粘贴并查集的实现
class Solution {
public:
    bool equationsPossible(vector<string>& equations) 
    {
        // 映射26个小写字母和下标[0, 25]之间的关系
        UnionFindSet ufs(26);
        // 1、先把所有==关系的元素集合到一起
        for(auto& e : equations)
        {
            if(e[1] == '=')
            {
                ufs.Union(e[0]-'a', e[3]-'a');
            }
        }
        // 2、反证:如果!=关系的元素在一个集合中就返回false
        for(auto& e : equations)
        {
            if(e[1] == '!' && (ufs.FindRoot(e[0]-'a') == ufs.FindRoot(e[3]-'a')))
            {
                return false;
            }
        }
        return true;
    }
};


第二题:计算朋友圈

题目连接


题目描述:

假设有N个用户,其中有些人是朋友,有些则不是。A和B是朋友,B和C是朋友,这样ABC就是一个朋友圈,请计算给定的朋友关系的朋友圈数。

给定一个 N * N 的矩阵 M,表示用户之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个人互为朋友关系,否则为不知道。你必须输出所有用户中的已知的朋友圈总数。


输入描述:

第一行输入为表示用户数N

第2到1+N行表示朋友之间关系,如果值为1表示有朋友关系


输出描述:

输出朋友圈数


示例1:


输入

3

1,1,0

1,1,1

0,1,1

输出

1

说明

第0,1,2个用户组成了一个朋友圈


示例2:


输入

3

1,1,0

1,1,0

0,0,1

输出

2

说明

第0个用户和第1个用户组成一个朋友圈,第2个用户组成一个朋友圈


解题思路:

遍历二维数组,如果是朋友就直接通过Union()把两个因素所在的集合合并,最后通过GetSet()获取并查集中集合的个数即为朋友圈数量。


注意输入的每个关系之间是用逗号分隔的,在解析时我是通过把每行先转为字符串在拿到数字的。


完整代码:


#include <vector>  
#include <string>
#include <iostream>
using namespace std;
// 这里粘贴并查集的实现
int main()
{
    int N = 0;
    cin >> N;
    UnionFindSet ufs(N);
    getchar();// 从缓冲区中拿走输入N后遗留下来的'\n'
    vector<vector<int>> vv(N, vector<int>(N));
    for (int i = 0; i < N; ++i)
    {
        string s;
        getline(cin, s);
        vector<int> tmp;
        for (auto e : s)
        {
            if (e == '0' || e == '1')
            {
                tmp.push_back(e-'0');
            }
        }
        for (int j = 0; j < N; ++j)
        {
            vv[i][j] = tmp[j];
            if (vv[i][j] == 1)
            {
                ufs.Union(i, j);
            }
        }
    }
    cout << ufs.GetSet() << endl;
    return 0;
}
相关文章
|
23天前
|
存储 算法
算法思想总结:模拟算法
算法思想总结:模拟算法
|
3月前
|
算法 测试技术 C#
利用广度优先或模拟解决米诺骨牌
利用广度优先或模拟解决米诺骨牌
|
11月前
|
C++
C++ --模拟实现搜索二叉树
#搜索二叉树 1. 搜索二叉树特点 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 每个结点的左右子树也分别为二叉搜索树
22 0
|
11月前
|
安全 C++
【C++】二叉搜索树模拟实现
二叉搜索树也称为二叉排序树。它或者是一个空树或者是有如下性质的二叉树: • 左子树上的所有节点的值小于根节点 • 右子树上的所有节点的值大于根节点 • 不存在值相同
|
设计模式 存储 测试技术
【C++】栈和队列的模拟实现 & 经典题目解析
【C++】栈和队列的模拟实现 & 经典题目解析
141 0
【C++】栈和队列的模拟实现 & 经典题目解析
|
Java
数据结构,Java实现递归回溯,寻找出迷宫路线,解决迷宫问题
数据结构,Java实现递归回溯,寻找出迷宫路线,解决迷宫问题
70 0
数据结构,Java实现递归回溯,寻找出迷宫路线,解决迷宫问题
|
存储 算法
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(三)
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(三)
|
人工智能
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(二)
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(二)
|
算法 索引
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(一)
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(一)
|
机器学习/深度学习
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(四)
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟
<代码随想录二刷>二分查找 | 双指针 | 滑动窗口 | 枚举模拟(四)