目录
1. 原理/概念
并查集(Union-Find)主要用来解决无向图的【动态连通性】问题,对于无向图的连通,一般具有如下几个性质:
- 自反性:节点 p 和 p 是连通的;
- 对称性:如果节点 p 和节点 q 连通,那么节点 q 和 p 也是连通的;
- 传递性:如果节点 p 和 q 连通, q 和 r 连通,那么 p 和 r 连通。
本文参考一个大神labuladong的算法小抄:Union-Find算法详解,主页在这。
注意,并查集只能用来解决无向图的连通性的问题,如果是有向图,要找其强连通分量,需要用tarjan算法,具体可以参考B站的一个视频:[算法]轻松掌握tarjan强连通分量。
2. 应用举例
Leetcode(990):等式方程的可满足性。就是一道典型的使用并查集的题目。、
类似的变型还有比如给定一群人的人数,然后接下来每次给出一个输入表示两个人是不是老乡,最后让你判断某两个人是不是老乡或者这群人互为老乡的人最多有多少。
其实上述都是一种动态连通性的问题,可以使用并查集算法,将同一类输入挂到同一个树下,使各个节点之间相互连通。
3. 抽象模型
并查集算法的抽象模型,包含两个主要操作:find和union。find操作查找两个节点是否在同一个树下(即是否属于同一个集合),union操作将两个节点所属的树进行合并(如果两个节点属于同一棵树,则不进行操作)。下面将算法抽象出一个通用的数据结构。
class UF {
private:
int count;
int* parent;
int* size;
public:
//构造和析构
UF(int n) :count(n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
UF(UF&) = delete;
UF& operator=(const UF&) = delete;
~UF(){
if (parent != nullptr) delete[] parent;
if (size != nullptr) delete[] size;
parent = nullptr;
size = nullptr;
}
//查找节点x所在树的根节点
int _find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
//将p和q连通,挂在一棵树下
void _union(int q, int p) {
int rootP = _find(p);
int rootQ = _find(q);
if (rootP == rootQ)return;//如果二者已经在同一棵树下,return
//否则,将二者所在的不同的树连通,小树挂在大树下面
if (size[rootP] > size[rootQ]) {
parent[rootQ] = parent[rootP];
size[rootP] += size[rootQ];
}
else {
parent[rootP] = parent[rootQ];
size[rootQ] += size[rootP];
}
count--;//挂完之后,将树的个数-1
}
//判断两个节点是否属于同一个树,即p和q是否连通
bool _connect(int p, int q) {
int rootP = _find(p);
int rootQ = _find(q);
return rootP == rootQ;
}
int _count() { return count; }
};
4. LeetCode(990):等式方程的可满足性
基于上述的并查集数据模型,“等式方程的可满足性”这道题的思路就是:先将所有等式两端的值进行union操作,然后再判断不等式两端的值是否在同一个集合中(在同一个集合中,return false;否则return true)。
具体代码如下:
class Solution {
public:
//并查集类
class UF{
private:
int count;//记录树的个数
int* parent;//总的元素个数
int* size;//每个元素为根的树的节点个数
public:
//构造和析构
UF(int n):count(n){
parent=new int[n];
size=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
size[i]=1;
}
}
UF(UF&) = delete;
UF& operator=(const UF&) = delete;
~UF(){
if (parent != nullptr) delete[] parent;
if (size != nullptr) delete[] size;
parent = nullptr;
size = nullptr;
}
//将p和q连通
void _union(int q,int p){
int rootP=_find(p);
int rootQ=_find(q);
if(rootP==rootQ)return;//如果二者已经在同一个树,不做操作
//否则,将二者所在的不同树挂在一起
//小树接到大树下面
if(size[rootP]>size[rootQ]){
parent[rootQ]=rootP;
size[rootP]+=size[rootQ];
}else{
parent[rootP]=rootQ;
size[rootQ]+=size[rootP];
}
count--;//挂完之后,将树的个数减一
}
//查找该节点的根
int _find(int x){
while(parent[x]!=x){
parent[x]=parent[parent[x]];
x=parent[x];
}
return x;
}
//判断两个节点是否属于同一个树
bool _connext(int p,int q){
int rootP=_find(p);
int rootQ=_find(q);
return rootP==rootQ;
}
int _count(){return count;}//返回集合中树的个数
};
//问题求解函数
bool equationsPossible(vector<string>& equations) {
//并查集Union-Find
UF uf(26);
//先让相等的字母连通
for(string& str:equations){
if(str[1]=='='){
int x=str[0]-'a';
int y=str[3]-'a';
uf._union(x,y);
}
}
//然后检查不等的字母是否是连通的
for(string& str:equations){
if(str[1]=='!'){
int x=str[0]-'a';
int y=str[3]-'a';
if(uf._connext(x,y))return false;
}
}
return true;
}
};