目录
在生活中,我们经常需要对某一些事物进行归类处理,即:将N个不同的元素划分为几个互不相交的集合。在初始状态,每个元素都属于一个独立的集合,在归一合并的过程中,经常需要查找某个元素属于哪一个集合,实现上面功能的数据结构,称之为并查集。
举一个生活中的例子,假设小卖部了有如下货物:苹果、香蕉、橘子、面包、薯片、牙膏、毛巾等,一开始进货的时候,这些货物是散装进货的,没有归类。一般而言,我们会将苹果、香蕉、橘子归类为水果,将面包和薯片归为零食,将牙膏和毛巾归为洗漱用品,这就相当于并查集查找元素所属的几何并进行归类的过程。初步归类后,继续将水果和零食统一归类为食物,这相当于合并两个类,并查集可以实现对类的合并。
并查集本质上是通过多颗树(森林)来实现的,每一颗树都表示一个独立的集合,使用双亲法表示树结构,即每个节点仅保存其父亲节点。如图1.1所示,将1~9十个数字分为三个集合,分别为{0,5,6,9}、{1,4,7}、{2,3,8},用三棵树表示对应关系,取{...}中第一个数据为根节点,使用vector
如图1.2所示,将图1.1中的{0,5,6,9}和{1,4,7}归为一类,那么1就变为了0的孩子节点,合并后的几集合以0为根节点,有7个元素,因此vector
我们假设并查集要管理的元素值从1~N,不考虑模板类型数据和vector
代码2.1:并查集类UnionFindSet的声明(UnionFindSet.hpp头文件)
- #pragma once
- #include
- #include
- #include
-
- // 并查集
- class UnionFindSet
- {
- public:
- UnionFindSet(size_t size);
- int FindRoot(int x); // 查找某个元素属于哪个集合(根下标)
- void Union(int x1, int x2); // 合并两个集合
- size_t Size() const; // 统计并查集内集合的个数
-
- private:
- std::vector<int> _ufs;
- };
代码2.2:并查集的实现(UnionFindSet.cpp源文件)
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include "UnionFindSet.hpp"
-
- UnionFindSet::UnionFindSet(size_t size)
- : _ufs(size, -1)
- { }
-
- // 查找某个元素属于哪个集合(根下标)
- int UnionFindSet::FindRoot(int x)
- {
- // 循环查找,直到_ufs[x] < 0
- while (_ufs[x] >= 0)
- {
- x = _ufs[x];
- }
-
- return x;
- }
-
- // 合并两个集合
- void UnionFindSet::Union(int x1, int x2)
- {
- int root1 = FindRoot(x1);
- int root2 = FindRoot(x2);
-
- if (root1 == root2)
- {
- return;
- }
-
- if (root1 > root2)
- {
- std::swap(root1, root2);
- }
-
- _ufs[root1] += _ufs[root2];
- _ufs[root2] = root1;
- }
-
- // 统计并查集内集合的个数
- size_t UnionFindSet::Size() const
- {
- // 遍历_ufs统计小于0的元素的个数
- size_t count = 0;
- for (const auto x : _ufs)
- {
- if (x < 0) ++count;
- }
- return count;
- }
如果并查集中毒元素数量过多,那么每次查找根节点都需要花费较大的成本,那么,就需要采取适当的手段来进行路径压缩。
路径压缩最常用的方法:每当查找完一个元素所属集合的根节点后,都将这个元素的父亲节点直接设备根节点,这样当第二次查找这个元素的根节点时,就只需要向上查找一层即可。
如图3.1所示,假设要查找5的根节点,并进行路径压缩,只需要在找到5的根节点0后,将5挂在0的下面即可。当然,如果元素的数量不是很大,就没必要考虑路径压缩,因为路径压缩本身也存在资源消耗。