文章目录
- 1、题目链接
- 2、题目描述
- 3、并查集思路
- 3.1、按秩合并
- 3.2、常用并查集代码
- 4、题目解析
1、题目链接
721. 账户合并
2、题目描述
3、并查集思路
并查集可以在很短的时间内合并不同的集合。它的思想为,一开始将不同单元单独作为一个结点,然后按等价条件进行合并,这个合并比单独使用集合合并快很多(因为单独使用集合这个数据结构,集合不能被真正合并,而且你需要快速找到是哪个集合,合并速度也没有并查集快)。我们一般使用整数下标作为一个结点。因此在涉及到字符串或者其他非整数个体时,我们可以使用哈希表将其映射到整数。
3.1、按秩合并
注意按秩合并不是按高度合并,因为高度很难维护。因为当路径压缩时高度减小,不能维护。
秩
是指节点个数。
3.2、常用并查集代码
class UnionFind{
public:
UnionFind(int n){
for(int i = 0;i < n; ++ i){
fa.emplace_back(i);
rank.emplace_back(1);
}
}
int Find(int x){
if(fa[x] == -1) return -1;
return fa[x] == x ? x : (fa[x] = Find(fa[x]));
}
void Union(int x, int y){
int fax = Find(x);
int fay = Find(y);
if(fax != fay){
if(rank[fax] > rank[fay]){
fa[fay] = fax;
rank[fax] ++;
}else{
fa[fax] = fay;
rank[fay] ++;
}
}
return;
}
private:
vector<int> fa;
vector<int> rank;
};
4、题目解析
我们使用哈希表将邮箱编号,之后按账户邮箱进行并查集合并,我们知道一个账户的邮箱是只能属于同一个用户,这些属于同一个用户的邮箱是完全等价的。
比如:
["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"]
"johnsmith@mail.com"
和"john00@mail.com"
属于同一个用户
"johnsmith@mail.com"
和"john_newyork@mail.com"
属于同一个用户
则他们三共同属于一个用户,它们可以当作一个等价类,这个等价关系本质上是通过用户来确定的。
当我们将这些邮箱合并后,由于还是整数集合,我们再将其转化为字符串集合即可。再通过账户来看它的邮箱属于哪个等价类,来确定它的邮箱有哪些,确定过的账户,等价类标记为-1
即可(等价类的父结点为-1
),防止重复。
这里我采用了另一个哈希表,从父亲映射到一个有序的字符串等价类集合。这样我们对于一个账户通过其第一个邮箱就能找到其对应的等价类集合了。然后标记这个集合下次不再记入答案。
- 给字符串编号
- 将邮箱转成编号按用户合并
- 以父亲为代表求出其邮箱集合等价类(为了方便,使用
set
可以顺序访问,就不需要再排序了) - 遍历账户取得其邮箱。
class UnionFind{
public:
UnionFind(int n){
for(int i = 0;i < n; ++ i){
fa.emplace_back(i);
rank.emplace_back(1);
}
}
int Find(int x){
if(fa[x] == -1) return -1;
return fa[x] == x ? x : (fa[x] = Find(fa[x]));
}
void Union(int x, int y){
int fax = Find(x);
int fay = Find(y);
if(fax != fay){
if(rank[fax] > rank[fay]){
fa[fay] = fax;
rank[fax] ++;
}else{
fa[fax] = fay;
rank[fay] ++;
}
}
return;
}
void tag(int x){//标记并查集
int fax = Find(x);
fa[fax] = -1;
return;
}
private:
vector<int> fa;
vector<int> rank;
};
class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
int id = 0;
//编号
for(auto & emails : accounts){
for(int i = 1; i < emails.size(); ++ i){
if(mp.count(emails[i]) == 0){
mp[emails[i]] = id ++;//编号
}
}
}
//并查集合并邮箱
UnionFind uf(id);
for(auto & emails : accounts){
int x = mp[emails[1]];//第一个邮箱
for(int i = 2; i < emails.size(); ++ i){
int y = mp[emails[i]];
uf.Union(x, y);//合并两个邮箱
}
}
//提取合并邮箱
unordered_map<int, set<string>> email;
for(auto & emails : accounts){
for(int i = 1; i < emails.size(); ++ i){
int y = uf.Find(mp[emails[i]]);
set<string> & st = email[y];
st.insert(emails[i]);
}
}
//确定账户
vector<vector<string>> ans;
for(auto & emails : accounts){
int x = mp[emails[1]];//第一个邮箱
if(uf.Find(x) == -1) continue;
vector<string> account;
account.emplace_back(emails[0]);
int fax = uf.Find(x);
set<string> & st = email[fax];
for(auto & t: st){
account.emplace_back(t);
}
ans.emplace_back(account);
uf.tag(x);//标记并查集为-1
}
return ans;
}
private:
unordered_map<string, int> mp;//字符串转为整数
};
这段代码使用并查集来合并邮箱账户,并输出合并后的结果。我们可以逐步解释这段代码的各个部分,以便更好地理解它的工作原理。
以下是代码详细解释:
代码概述
- UnionFind 类:实现并查集的数据结构,用于合并和查找元素。
- Solution 类:实现
accountsMerge
方法,用于合并邮箱账户。
UnionFind 类解释
- 构造函数:初始化
fa
(父节点数组)和rank
(秩数组),每个元素的父节点初始化为自身,秩初始化为1。 - Find 方法:路径压缩,查找元素的根节点并将访问的所有节点直接连接到根节点。
- Union 方法:按秩合并,将秩较小的树合并到秩较大的树上。
- tag 方法:标记节点为 -1,表示该节点已经处理过。
Solution 类解释
-
编号阶段:
- 使用
mp
将每个邮箱字符串映射到一个唯一的整数ID。 - 遍历
accounts
,给每个邮箱分配一个唯一ID。
- 使用
-
合并邮箱:
- 初始化
UnionFind
对象uf
,大小为所有邮箱的数量。 - 遍历
accounts
,对每个账户,将第一个邮箱和其他邮箱进行合并。
- 初始化
-
提取合并邮箱:
- 使用
email
哈希表将每个根ID映射到其对应的邮箱集合。 - 遍历
accounts
,将每个邮箱的根ID作为键,将邮箱加入到相应的集合中。
- 使用
-
确定账户:
- 初始化结果向量
ans
。 - 再次遍历
accounts
,根据邮箱的根ID构建每个用户的账户信息,并将结果加入ans
。 - 使用
uf.tag(x)
标记已经处理的根ID,避免重复处理。
- 初始化结果向量
注意这里标记等价类(tag)也可以改为将集合清空,这样父亲对应的是一个空集合就说明这个集合已经处理过的,不过这样的时间复杂度高一点。