一、基本概念
并查集(Union-Find)是一种用于处理不相交集合的合并和查询问题的数据结构。它主要用于处理一些不交集的合并及查询问题,例如网络中的连通分量、图的边联通性等。
这种数据结构支持两种操作:查找(Find)和合并(Union)。
并查集通常使用一个整数数组parent[]来表示,其中parent[i]表示元素i的父节点。如果parent[i] = i,则i是一个根节点,即它是集合的代表。
并查集的主要操作有:
- Find: 查找元素所在的集合,即集合的代表。
- Union: 将两个元素所在的集合合并为一个集合。
二、并查集的扩展算法
- 路径压缩:
为了提高效率,路径压缩技术应运而生。路径压缩是在执行Find操作时实现的,其目的是使得从任何一个节点到根节点的路径尽可能短,从而加快未来的查找速度。 - 按秩合并:
另一种优化技术是按秩合并(Union by Rank)。秩可以理解为树的高度,我们总是将更矮的树连接到更高的树上。这样可以避免树过深,从而导致操作效率降低。
三、C++实现
以下是一个使用路径压缩和按秩合并的并查集的C++实现:
#include <iostream>
#include <vector>
class UnionFind {
private:
std::vector<int> parent; // 存储父节点
std::vector<int> rank; // 秩(可以理解为树的高度)
public:
// 构造函数,初始化并查集
UnionFind(int size) : parent(size), rank(size, 0) {
for (int i = 0; i < size; i++) {
parent[i] = i; // 初始时,每个元素的父节点是它自己
}
}
// 查找操作,带路径压缩
int find(int p) {
if (parent[p] != p) {
parent[p] = find(parent[p]); // 路径压缩
}
return parent[p];
}
// 合并操作,按秩合并
void unionSets(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP != rootQ) { // 只有根节点不同,才需要合并
if (rank[rootP] > rank[rootQ]) {
parent[rootQ] = rootP; // 小树接到大树下面
} else if (rank[rootP] < rank[rootQ]) {
parent[rootP] = rootQ; // 小树接到大树下面
} else {
parent[rootQ] = rootP; // 如果相同,任选一个作为根,并增加其秩
rank[rootP]++;
}
}
}
};
int main() {
UnionFind uf(10); // 创建大小为10的并查集
uf.unionSets(1, 2);
uf.unionSets(3, 4);
uf.unionSets(1, 4); // 合并1/2集合与3/4集合
// 输出元素1的代表
std::cout << "The representative of element 1 is: " << uf.find(1) << std::endl;
// 输出元素3的代表
std::cout << "The representative of element 3 is: " << uf.find(3) << std::endl;
return 0;
}
代码解释
UnionFind 类:包含两个主要数组,parent和rank。parent数组用于跟踪每个元素的父节点,而rank数组记录了每个树的秩(或高度)。
Find 函数:实现了路径压缩。这是通过递归将元素直接链接到其根节点来实现的。路径压缩确保了后续的查找操作更加高效。
Union 函数:按秩合并两个集合。较低秩的根节点将指向较高秩的根节点。如果两棵树的秩相同,选择其中一个树作为根,并增加其秩。
这种结构和操作方法显著提高了并查集处理大量数据和查询的效率。
四、应用场景
并查集是一种非常有效的数据结构,用于处理一些不交集的合并及查询问题。这里有几个具体的应用场景:
- 网络连接:并查集可以用来检查网络中的计算机是否相互连接。例如,在局域网或社交网络中,可以用并查集来快速找出两台计算机是否通过某种方式连接。
- 图的连通分量:在处理无向图时,可以用并查集来识别和计算图中的连通分量数量。每个连通分量可以用一个集合来表示,合并操作可以帮助快速将两个节点连在一起。
- 最小生成树:在计算最小生成树的Kruskal算法中,可以用并查集来检测添加的边是否会形成环路。这是通过检查边的两个节点是否已经在同一集合中来实现的。
- 动态连通性:在需要多次添加和查询操作的动态网络中,如在实时系统中动态地添加路由和查询路由信息,可以利用并查集的动态连通性检查功能。
- 图像处理:在图像处理中,可以使用并查集来进行区域标记或计算每个连通区域的大小,特别是在处理二值图像时,用来识别和标记相连的组件。
- 数学中的等价关系:并查集可以用来维护和查询元素之间的等价关系,如在抽象代数、集合理论中的等价类划分。
- 路径问题:在某些路径优化问题中,如迷宫的路径寻找或最短路径问题,可以用并查集快速判断两点之间是否存在路径。
这些应用场景展示了并查集在多种领域的实用性和效率,特别是在需要频繁执行合并集合和查询集合代表元素的操作时。
五、例题
力扣721.账户合并
给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。
现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。
示例 1:
输入:accounts = [[“John”, “johnsmith@mail.com”, “john00@mail.com”], [“John”, “johnnybravo@mail.com”], [“John”, “johnsmith@mail.com”, “john_newyork@mail.com”], [“Mary”, “mary@mail.com”]]
输出:[[“John”, ‘john00@mail.com’, ‘john_newyork@mail.com’, ‘johnsmith@mail.com’], [“John”, “johnnybravo@mail.com”], [“Mary”, “mary@mail.com”]]
解释:
第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 “johnsmith@mail.com”。
第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
可以以任何顺序返回这些列表,例如答案 [[‘Mary’,‘mary@mail.com’],[‘John’,‘johnnybravo@mail.com’],
[‘John’,‘john00@mail.com’,‘john_newyork@mail.com’,‘johnsmith@mail.com’]] 也是正确的。
示例 2:
输入:accounts = [[“Gabe”,“Gabe0@m.co”,“Gabe3@m.co”,“Gabe1@m.co”],[“Kevin”,“Kevin3@m.co”,“Kevin5@m.co”,“Kevin0@m.co”],[“Ethan”,“Ethan5@m.co”,“Ethan4@m.co”,“Ethan0@m.co”],[“Hanzo”,“Hanzo3@m.co”,“Hanzo1@m.co”,“Hanzo0@m.co”],[“Fern”,“Fern5@m.co”,“Fern1@m.co”,“Fern0@m.co”]]
输出:[[“Ethan”,“Ethan0@m.co”,“Ethan4@m.co”,“Ethan5@m.co”],[“Gabe”,“Gabe0@m.co”,“Gabe1@m.co”,“Gabe3@m.co”],[“Hanzo”,“Hanzo0@m.co”,“Hanzo1@m.co”,“Hanzo3@m.co”],[“Kevin”,“Kevin0@m.co”,“Kevin3@m.co”,“Kevin5@m.co”],[“Fern”,“Fern0@m.co”,“Fern1@m.co”,“Fern5@m.co”]]
提示:
1 <= accounts.length <= 1000
2 <= accounts[i].length <= 10
1 <= accounts[i][j].length <= 30
accounts[i][0] 由英文字母组成
accounts[i][j] (for j > 0) 是有效的邮箱地址
题解:
class UnionFind{
public:
vector<int> parent;
UnionFind(int n){
parent.resize(n);
for(int i=0;i<n;i++)
{
parent[i] = i;
}
}
void UnionSet(int index1,int index2){
parent[find(index2)] = find(index1);
}
int find(int index){
if(parent[index]!=index){
parent[index] = find(parent[index]);
}
return parent[index];
}
};
class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
map<string,int> EmailToIndex;
map<string,string> EmailToName;
int EmailCount=0;
for(auto& account:accounts){
string& name = account[0];
int size = account.size();
for(int i=1;i<size;i++){
if(!EmailToIndex.count(account[i])){
string& email = account[i];
EmailToIndex[email] = EmailCount++;
EmailToName[email] = name;
}
}
}
UnionFind uf(EmailCount);
for(auto& account:accounts){
string& FirstEmail = account[1];
int FirstEmailIndex = EmailToIndex[FirstEmail];
int size = account.size();
for(int i=2;i<size;i++)
{
string& NextEmail = account[i];
int NextEmailIndex = EmailToIndex[NextEmail];
uf.UnionSet(FirstEmailIndex,NextEmailIndex);
}
}
map<int,vector<string>> IndexToEmail;
for(auto&[email,_]:EmailToIndex){
int index = uf.find(EmailToIndex[email]);
vector<string>& account = IndexToEmail[index];
account.emplace_back(email);
IndexToEmail[index] = account;
}
vector<vector<string>> marge;
for(auto& [_,email]:IndexToEmail){
sort(email.begin(),email.end());
string& name = EmailToName[email[0]];
vector<string> account;
account.emplace_back(name);
for(auto& emails:email){
account.emplace_back(emails);
}
marge.emplace_back(account);
}
return marge;
}
};
六、总结
并查集是一种高效的数据结构,专门用于处理不相交集合的合并和查询问题。它通过一种称为“路径压缩”的优化技术来快速寻找一个元素的根节点,同时利用“按秩合并”的策略来减少查找路径的长度,从而在实际应用中达到近乎常数时间的操作效率。
并查集广泛应用于多种场景,如网络连接的快速检测、图的连通分量分析、最小生成树的构建、动态连通性问题、图像处理中的连通区域标记等。在这些应用中,它主要处理组件连接性的问题,使得用户能够高效地查询和合并节点集合。
因此,并查集不仅是理解计算机科学中图论和网络算法的一个重要工具,也是优化数据处理和增强程序性能的关键技术之一。