网上统统都是用的传统并查集的模板,说实话我不太喜欢那种并查集的写法,于是研究了一个下午终于写出来了自己满意的并查集写法(其实更像模拟),而且速度与内存也比较优秀。
关键是:【一个人所在的社交团体】和【他喜欢的每一个爱好的所有爱好者所在的社交图体】同属于一个Social Cluster
也就是说:让【一个人所在的集合】和【他喜欢的每一个爱好的所有爱好者所在的集合】合并(所有爱好者会包括自己,处不处理都没关系,跳过会更快),就是我们的任务。
不用传统的并查集
- 用
hobby_lover
邻接表来存储某一个爱好的所有爱好者 - 用
person_hobby
邻接表来存储某一个人的所有爱好 - 用
map
来创建[集合编号:集合]
的映射(命名为Cluster
),用第一个进入集合的人的编号来作为集合的编号,也就是Cluster
中的键(也可以理解为并查集中的根节点)),在并查集中不同集合不会有相交元素,所以我用vector
来代替set
了; - 用
setID
数组来记录每一个人所在的集合的编号(也可以理解为并查集中的根节点) - 对于并查集中的
findFather()
查找一个节点所在集合的根节点的操作,就可以直接用setID[某一个人的编号]
(查到的其实是集合的编号)------- 而查找两个元素是否是在同一个集合就可以用setID[a] == setID[b]
来判断 - 对于并查集的两个集合的
Union()
合并操作,其实将两个集合的vector
直接push_back
和pop_back
以及取back
处的元素就可以完成合并了(并查集中这两个集合不会有相交的元素的)
# include <bits/stdc++.h>
using namespace std;
int N;
vector<int> hobby_lover[1010]; // 某一个爱好的每一个爱好者
vector<int> person_hobby[1010]; // 某一个人的每一个爱好
map<int, vector<int> > Cluster; // 以第一个加入集合的人作为集合的编号(键),值为一个社交团体集合
vector<int> setID(1010, 0); // 存放每个人所在的集合编号,为0说明集合不存在(这个人不存在于任何集合)
int main(){
// 输入
cin >> N;
for(int person = 1;person <= N;++person){
int K;
scanf("%d: ", &K);
for(int j = 0;j < K;++j){
int hobby;
cin >> hobby;
hobby_lover[hobby].push_back(person);
person_hobby[person].push_back(hobby);
}
}
// 算法开始
for(int person = 1;person <= N;++person){ // 遍历每一个人
// 【这个人】指代每一次的【person】
// 如果【这个人】没在任何集合那就让它自创一个集合,
// 于是他就一定是集合的根节点,于是它的编号也就是person值就是它所属集合的编号
if(setID[person] == 0){
setID[person] = person;
Cluster[setID[person]].push_back(person);
}
for(int hobby: person_hobby[person]){ // 遍历【这个人】的每一个爱好
// 《某个爱好者》指代每一次的【lover】
for(int lover: hobby_lover[hobby]){ // 遍历【这个人】的某一个爱好的所有爱好者,让【这个人】和对应的爱好者并入一个集合
if(setID[lover] == 0){ // 如果《某个爱好者》没在任何集合,那就让他并入【这个人】所在的集合
setID[lover] = setID[person];
Cluster[setID[person]].push_back(lover);
}
else { // 如果《某个爱好者》有所在集合,那就将《某个爱好者》所在的集合并入【这个人】所在的集合
if(setID[person] == setID[lover]) continue; // 如果两个要合并的集合是同一个集合,那就跳过
for(int i = Cluster[setID[lover]].size() - 1;i >= 0 ;i--){
int inp = Cluster[setID[lover]].back(); // 取得《某个爱好者》所在集合的成员
Cluster[setID[person]].push_back(inp);
Cluster[setID[lover]].pop_back();
}
Cluster.erase(setID[lover]); // 去掉被合并的集合
setID[lover] = setID[person]; // 合并后记得更新《某个爱好者》所在集合的编号为【这个人】的集合
}
}
}
}
vector<int> rst;
cout << Cluster.size() << endl;
for(auto s: Cluster){
rst.push_back(s.second.size()); // 统计每个社交团体的数量
}
sort(rst.begin(), rst.end(), greater<int>());
for(int i = 0;i < rst.size();++i){
cout << rst[i] << (i == rst.size()-1 ? "\n" : " ");
}
return 0;
}
/*
hobby people
[1] : 7
[2] : 1
[3] : 3 5
[4] : 2 4 6 8
[5] : 3 7
[6] : 7
[7] : 1
[8] : 7
[9] :
[10] : 1
*/
/*
people hobby
1 : 2 7 10
2 : 4
3 : 5 3
4 : 4
5 : 3
6 : 4
7 : 6 8 1 5
8 : 4
*/
传统并查集
一开始每一个人就是一个集合,每次都合并有相同爱好的人
每次的person和hobby_person[hb]都是有相同爱好hb的,所以每次将他们合并
# include <bits/stdc++.h>
using namespace std;
int N;
int father[1010];
int isRoot[1010];
int hobby_person[1010] = {0};
int findFather(int v){
if(v == father[v]) return v;
father[v] = findFather(father[v]);
return father[v];
}
void Union(int a, int b){
int fa = findFather(a);
int fb = findFather(b);
if(fa != fb) father[fa] = fb;
}
void init(){
for(int i = 1;i <= N;++i){
father[i] = i;
isRoot[i] = false;
}
}
int main(){
cin >> N;
init();
int K, hb;
for(int person = 1;person <= N;++person){ // 对每个人
scanf("%d: ", &K);
for(int j = 0;j < K;++j){ // 对某个人的每个爱好
cin >> hb;
if(hobby_person[hb] == 0) // 让这个爱好的第一个爱好者成为一个社交圈子
hobby_person[hb] = person;
else // 合并有相同爱好hb的两个社交圈子
// Union(person, hobby_person[hb]); // 这么写也没问题,毕竟在Union函数中都会找到father的
Union(person, findFather(hobby_person[hb]));
}
}
for(int i = 1;i <= N;++i)
isRoot[findFather(i)]++;
int cnt = 0;
for(int i = 1;i <= N;++i)
if(isRoot[i] != 0)
cnt++;
cout << cnt << endl;
sort(isRoot+1, isRoot+N+1, greater<int>());
for(int i = 1;i <= cnt;++i)
cout << isRoot[i] << (i == cnt?'\n':' ');
return 0;
}