目录
1. 并查集的概念
2. 并查集的实现
3. 并查集的应用
3.1 力扣LCR 116. 省份数量
解析代码1
解析代码2
3.2 力扣990. 等式方程的可满足性
解析代码
本篇完。
写在前面:
此高阶数据结构系列,虽然放在⑤数据结构与算法专栏,但还是作为一个拓展学习,建议跳过第⑤序号跟着其它专栏序号学,当时是想着要期末考和考研的同学,考到图才开这个专栏的吧,其他不急的同学可以在学完MySQL专栏后再看,此系列也放在了⑩其它高阶数据结构专栏,这里简单学习并查集是为了下一个数据结构“图”的学习。
1. 并查集的概念
- 并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
- 并查集通常用森林来表示,森林中的每棵树表示一个集合,树中的结点对应一个元素。
虽然利用其它数据结构也能完成不相交集合的合并及查询,但在数据量极大的情况下,其耗费的时间和空间也是极大的。
在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一 个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)。
并查集是多个独立集合的合集,用于表示数据之间的关系,并查集中的每一个集合是用多叉树来表示的。
比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,武汉招3人,10个人来自不 同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个 数。数组中某个位置的值为负数,表示该位置是树的根,这个负数的绝对值表示的这棵树(集合)中数据的个数,因为刚开始每个人各自属于一个集合,所以将数组中的位置都初始化为-1。
毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起上路,于是:西安学生小分队s1={0,6,7,8},成都学生小分队s2={1,4,9},武汉学生小分队s3={2,3,5}就相互认识了,10个人形成了三个小团体。假设右三个群主0,1,2担任队长,负责大家的出行。
一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。
从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同 学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3 个人(包含队长1)。 仔细观察数组中内变化,可以得出以下结论:
- 数组的下标对应集合中元素的编号。
- 数组中如果为负数,负号代表根,数字代表该集合中元素个数。
- 数组中如果为非负数,代表该元素双亲在数组中的下标。
在公司工作一段时间后,西安小分队中8号同学与成都小分队1号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子:
现在0集合有7个人,2集合有3个人,总共两个朋友圈。通过以上例子可知,并查集一般可以解决一下问题:
- 查找元素属于哪个集合:沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)。
- 查看两个元素是否属于同一个集合:沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在。
- 将两个集合归并成一个集合:将两个集合中的元素合并,将一个集合名称改成另一个集合的名称。
- 集合的个数:遍历数组,数组中元素为负数的个数即为集合的个数。
2. 并查集的实现
代码实现还是很简单的,直接放出代码:(建议复制到自己编译器跟着注释一起看)
#pragma once
#include <iostream>
#include <vector>
using namespace std;
class UnionFindSet
{
private:
vector<int> _ufs;
public:
UnionFindSet(size_t size) // 初始时,将数组中元素全部设置为1
: _ufs(size, -1)
{}
int FindRoot(int index) // 给一个元素的编号,找到该元素所在集合的名称
{
int root = index;
while (_ufs[root] >= 0) // 如果数组中存储的是负数,找到,否则一直继续
{
root = _ufs[root];
}
while (_ufs[index] >= 0) // 路径压缩
{
int parent = _ufs[index];
_ufs[index] = root;
index = parent;
}
return index;
}
bool InSet(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
bool Union(int x1, int x2) // 合并两个集合
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2) // x1已经与x2在同一个集合
return false;
if (abs(_ufs[root1]) < abs(_ufs[root2])) // 控制数据量小的往大的集合合并
swap(root1, root2);
_ufs[root1] += _ufs[root2]; // 负号代表根,数字代表该集合中元素个数
_ufs[root2] = root1; // 将其中一个集合名称改变成另外一个
return true;
}
size_t Count() const // 数组中负数的个数,即为集合的个数
{
size_t count = 0;
for (auto e : _ufs)
{
if (e < 0)
++count;
}
return count;
}
};
void TestUFS()
{
UnionFindSet u(10);
u.Union(0, 6);
u.Union(7, 6);
u.Union(7, 8);
u.Union(1, 4);
u.Union(4, 9);
u.Union(2, 3);
u.Union(2, 5);
u.FindRoot(6);
u.FindRoot(9);
cout << u.Count() << endl;
}
3. 并查集的应用
直接复制上面并查集的代码到力扣写两道题:
3.1 力扣LCR 116. 省份数量
LCR 116. 省份数量
难度 中等
有 n
个城市,其中一些彼此相连,另一些没有相连。如果城市 a
与城市 b
直接相连,且城市 b
与城市 c
直接相连,那么城市 a
与城市 c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n
的矩阵 isConnected
,其中 isConnected[i][j] = 1
表示第 i
个城市和第 j
个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]] 输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]] 输出:3
提示:
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j]
为1
或0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
注意:本题与主站 547 题相同: 547. 省份数量
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
}
};
解析代码1
直接复制并查集过来:
class UnionFindSet
{
private:
vector<int> _ufs;
public:
UnionFindSet(size_t size) // 初始时,将数组中元素全部设置为1
: _ufs(size, -1)
{}
int FindRoot(int index) // 给一个元素的编号,找到该元素所在集合的名称
{
int root = index;
while (_ufs[root] >= 0) // 如果数组中存储的是负数,找到,否则一直继续
{
root = _ufs[root];
}
while (_ufs[index] >= 0) // 路径压缩
{
int parent = _ufs[index];
_ufs[index] = root;
index = parent;
}
return index;
}
bool InSet(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
bool Union(int x1, int x2) // 合并两个集合
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2) // x1已经与x2在同一个集合
return false;
if (abs(_ufs[root1]) < abs(_ufs[root2])) // 控制数据量小的往大的集合合并
swap(root1, root2);
_ufs[root1] += _ufs[root2]; // 负号代表根,数字代表该集合中元素个数
_ufs[root2] = root1; // 将其中一个集合名称改变成另外一个
return true;
}
size_t Count() const // 数组中负数的个数,即为集合的个数
{
size_t count = 0;
for (auto e : _ufs)
{
if (e < 0)
++count;
}
return count;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
UnionFindSet ufs(isConnected.size());
for (size_t i = 0; i < isConnected.size(); ++i)
{
for (size_t j = 0; j < isConnected[i].size(); ++j)
{
if (isConnected[i][j] == 1) // 合并集合
{
ufs.Union(i, j);
}
}
}
return ufs.Count();
}
};
解析代码2
用数组模拟并查集:
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
vector<int> ufs(isConnected.size(), -1); // 手动控制并查集
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 cnt = 0;
for(auto e : ufs)
{
if(e < 0)
++cnt;
}
return cnt;
}
};
3.2 力扣990. 等式方程的可满足性
990. 等式方程的可满足性
难度 中等
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i]
的长度为 4
,并采用两种不同的形式之一:"a==b"
或 "a!=b"
。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true
,否则返回 false
。
示例 1:
输入:["a==b","b!=a"] 输出:false 解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。
示例 2:
输入:["b==a","a==b"] 输出:true 解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:
输入:["a==b","b==c","a==c"] 输出:true
示例 4:
输入:["a==b","b!=c","c==a"] 输出:false
示例 5:
输入:["c==c","b==d","x!=z"] 输出:true
提示:
1 <= equations.length <= 500
equations[i].length == 4
equations[i][0]
和equations[i][3]
是小写字母equations[i][1]
要么是'='
,要么是'!'
equations[i][2]
是'='
class Solution {
public:
bool equationsPossible(vector<string>& equations) {
}
};
解析代码
并查集的变形,思路:
- 将所有"=="两端的字符合并到一个集合中。
- 检测"!=" 两端的字符是否在同一个集合中,如果在,不满足,如果不在,满足。
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& e : equations) // 第一遍,先把相等的值加到一个集合中
{
if(e[1] == '=')
{
int root1 = findRoot(e[0] - 'a');
int root2 = findRoot(e[3] - 'a');
if(root1 != root2)
{
ufs[root1] += ufs[root2];
ufs[root2] = root1;
}
}
}
for(auto& e : equations) // 第二遍,判断相等在不在一个集合,在就相悖了
{
if(e[1] == '!')
{
int root1 = findRoot(e[0] - 'a');
int root2 = findRoot(e[3] - 'a');
if(root1 == root2)
return false;
}
}
return true;
}
};
本篇完。
这里简单学习并查集更多是为了下一个数据结构“图”的学习,一些竞赛的OJ也会用到并查集。