并查集原理
一般可以用数组来表示并查集,数据的下标就是每个数据的编号,对应的值如果是负数,那么就代表它自成一个集合,也就是一个根结点。如图,初始值一般都是负数,也就是每个元素都自成一个集合,如果一个元素是根,那么负数的绝对值则代表这个树有多少结点;如果不是负数,那么则说明这个元素不是根,且对应的值存的是父亲结点的下标。
实际应用中也可以通过加入哈希表来设置更多的映射关系,这样的话可以将数组的下标作为key值,然后可以存人名这些。
所以并查集其实也是多棵树组成的森林。
用并查集表示就是
元素之间可以合并,并且可以制定合并规则。集合与集合之间也可以合并。
综上,我们可以知道并查集可以解决以下问题:
1.查找某个元素属于哪个集合。沿着数组表示的树形关系向上可以一直找到根。
2.查看两个元素是否属于同一个集合。可以看找到的根是否相同,以此来判断是否属于同一个集合。
3.可以将两个集合归并成一个集合。
4.求集合的个数。其实也就是根的个数,可以遍历数组,记录下标为负数的元素即是集合的个数。
并查集简单实现
#pragma once
#include <vector>
//可以设计成模板,加入哈希表来存放更多的映射关系
class UnionFindSet
{
public:
UnionFindSet(int size)
:_set(size, -1)
{}
~UnionFindSet()
{}
size_t FindRoot(int x)
{
int root = x;
while (_set[root] >= 0)
root = _set[root]; // 向上找到根
while (_set[x] >= 0) // 向上遍历,依次修改父亲的父亲结点,使其变为根结点的儿子。// 路径压缩
{
int parent = _set[x];
_set[x] = root;
x = parent;
}
return root;
}
void Union(int x1, int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (abs(_set[root1]) < abs(_set[root2])) // 小优化,每次合并都合并到结点更多的树中,可以减少层数
std::swap(_set[root1], _set[root2]);
if (root1 != root2) // 如果相等说明在一个集合内,没必要合并
{
_set[root1] += _set[root2];
_set[root2] = root1; // 这里默认合并到root1
}
}
int SetCount() // 找根结点个数
{
size_t count = 0;
for (size_t i = 0; i < _set.size(); ++i)
{
if (_set[i] < 0)
count++;
}
return count;
}
private:
std::vector<int> _set;
};
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);
std::cout << u.SetCount() << std::endl;
}
另外说下压缩路径的问题,当并查集的数据非常大时,我们要找到这个元素的根,可能就需要向上找很久,有一种解决方案就是,在每次查找的时候,如果它的父亲结点不是根结点的话,就将它放到根结点的儿子结点。这样就能减少遍历的次数。
并查集虽然是一种数据结构,但是有时候又可以是一种解决问题的思路。
比如这道题,可以直接手搓一个简单的并查集,很容易就秒掉。
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
vector<int> set(n,-1);
auto findRoot = [&set](int x)
{
while(set[x] >= 0)
x = set[x];
return x;
};
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
if(isConnected[i][j] == 1)
{
int root1 = findRoot(i);
int root2 = findRoot(j);
if(root1 != root2)
{
set[root1] += set[root2];
set[root2] = root1;
}
}
}
}
int count = 0;
for(auto &x : set)
{
if(x < 0)
count++;
}
return count;
}
};
总结并查集的特点:
1.一个位置的值是负数,那么它就是树的根,这个负数的绝对值就是这个树结点的个数。
2.一个位置的值是正数,那么这个正数就是它父亲结点的下标。