去 OI-Wiki 了解更多:
1. 并查集
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
2. 并查集应用
并查集与 Kruskal 重构树有许多共通点,而并查集的优化(按秩合并)正是启发式合并思想的应用。因此灵活运用并查集可以方便地处理许多与连通性有关的图论问题。
快速了解(以 L2-024 部落 为例):
问题引入:
在一个社区里,每个人都有自己的小圈子,还可能同时属于很多不同的朋友圈。我们认为朋友的朋友都算在一个部落里,于是要请你统计一下,在一个给定社区中,到底有多少个互不相交的部落?并且检查任意两个人是否属于同一个部落。
思路分析:
并查集可以使用一维数组很方便地维护每个人的归属圈子,从而便于我们判断部落的独立性和人与人之间的连通性。并查集的基本操作如下:
int f[10005];
int find(int x){
if(f[x]==x) return f[x];
return f[x]=find(f[x]);
}
void merge(int x, int y){
int a=find(x), b=find(y);
if(a!=b) f[b]=a;
}
int main(){
for(int i=0; i<10005; i++) f[i]=i;
}
通过初始化,每一个人都和自己相连。
find()
:用于查询每个人归属的源头,使用递归可以实现对路径的压缩。
merge()
:寻找两个人归属的集合是否有共同的源头,如果不同只需将他们的源头相连即可。
查询时,只需查询他们是否有共同的源头便可以得知他们是否属于一个部落。
对于部落是否相交,我们可以另开一个数组存储每个部落的第一个人,如果两个部落不相交,那么这两个人的源头也必不相同。
看懂别人的代码不是目的,自己写一遍才是正真掌握了喵!
完整代码:
#include<iostream>
#include<set>
using namespace std;
int f[10005], s[10005];
set<int> st, bl;
int find(int x){
if(f[x]==x) return f[x];
return f[x]=find(f[x]);
}
void merge(int x, int y){
int a=find(x), b=find(y);
if(a!=b) f[b]=a;
}
int main(){
for(int i=0; i<10005; i++) f[i]=i;
int n, k, p;
cin >> n;
for(int i=1; i<=n; i++){
cin >> k >> s[i];
st.insert(s[i]);
while(--k){
cin >> p;
st.insert(p);
merge(s[i], p);
}
}for(int i=1; i<=n; i++) bl.insert(find(s[i]));
cout << st.size() << ' ' << bl.size() << "\n";
cin >> n;
while(n--){
cin >> k >> p;
if(find(k)==find(p)) cout << "Y\n";
else cout << "N\n";
}
}
天梯赛相关题目:
L3-003 社交集群
别看是L3,其实比L2的题还简单,和上一题连代码都几乎一样。
#include<iostream>
#include<map>
#include<set>
using namespace std;
int f[1005], p[1005];
map<int, int> mp;
multiset<int, greater<int>> st;
int find(int x){
if(f[x]==x) return f[x];
return f[x]=find(f[x]);
}
void merge(int x, int y){
int a=find(x), b=find(y);
if(a!=b) f[b]=a;
}
int main(){
for(int i=0; i<1005; i++) f[i]=i;
char _;
int n, k, h;
cin >> n;
for(int i=1; i<=n; i++){
cin >> k >> _ >> p[i];
while(--k){
cin >> h;
merge(p[i], h);
}
}for(int i=1; i<=n; i++) mp[find(p[i])]++;
for(auto q:mp) st.insert(q.second);
cout << st.size() << "\n" << *st.begin();
st.erase(st.begin());
for(auto q:st) cout << ' ' << q;
cout << "\n";
}
L2-010 排座位
并查集+分类讨论,只需要在原模板的基础上判断两个人是否敌对。
#include<iostream>
#include<map>
#include<set>
using namespace std;
int f[105];
map<int,set<int>> mp;
int find(int x){
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
void merge(int x, int y){
int a=find(x), b=find(y);
if(a!=b) f[b]=a;
}
int main(){
int n, m, k, a, b, p;
cin >> n >> m >> k;
for(int i=1; i<=n; i++) f[i]=i;
while(m--){
cin >> a >> b >> p;
if(p==1) merge(a, b);
else{
mp[a].insert(b);
mp[b].insert(a);
}
}while(k--){
cin >> a >> b;
if(find(a)==find(b)){
if(mp.count(a) && mp[a].count(b))
cout << "OK but...\n";
else cout << "No problem\n";
}else{
if(mp.count(a) && mp[a].count(b))
cout << "No way\n";
else cout << "OK\n";
}
}
}
L2-007 家庭房产
并查集+结构体排序。前置文章:面向天梯赛编程(L2):结构体排序
这里对 merge()
进行了修改 if(a!=b) f[max(a,b)]=min(a, b);
以得到家庭成员的最小编号。
#include<iostream>
#include<unordered_map>
#include<unordered_set>
#include<algorithm>
using namespace std;
int f[10001];
unordered_map<int, double> nm, sq, pn;
unordered_set<int> st, mk;
struct family{
int id;
int pn;
double nm;
double sq;
} fml[10001];
bool cmp(family a, family b){
if(a.sq != b.sq) return a.sq > b.sq;
return a.id < b.id;
}
int find(int x){
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
void merge(int x, int y){
int a=find(x), b=find(y);
if(a!=b) f[max(a,b)]=min(a, b);
}
int main(){
for(int i=0; i<=10000; i++) f[i]=i;
int n, a, b, c, i=0;
scanf("%d", &n);
while(n--){
scanf("%d %d %d", &a,&b,&c);
st.insert(a);
if(b+1) merge(a, b), st.insert(b);
if(c+1) merge(a, c), st.insert(c);
scanf("%d", &c);
while(c--){
scanf("%d", &b);
merge(a, b);
st.insert(b);
}scanf("%d %d",&b,&c);
nm[find(a)] += b;
sq[find(a)] += c;
}for(auto p:nm){
if(find(p.first)!=p.first){
nm[find(p.first)] += p.second;
sq[find(p.first)] += sq[p.first];
mk.insert(p.first);
}
}while(!mk.empty()){
nm.erase(*mk.begin());
sq.erase(*mk.begin());
mk.erase(mk.begin());
}for(auto p:st) pn[find(p)]++;
for(auto p:nm){
fml[i].id=p.first;
fml[i].pn=pn[p.first];
fml[i].nm=p.second/fml[i].pn;
fml[i].sq=sq[p.first]/fml[i].pn;
i++;
}sort(fml, fml+i, cmp);
printf("%d\n", i);
for(int _=0; _<i; _++)
printf("%04d %d %.3f %.3f\n", fml[_].id, fml[_].pn, fml[_].nm, fml[_].sq);
}
题外话
L2-025 分而治之
看到网上有使用并查集检查连通性的题解,自己写到一半发现,既然要使所有城市孤立无援,感觉不如把并查集删了,直接判断每条路是否连接着被攻下的城市。如果路都没了,城市自然孤立无援了。
#include<iostream>
#include<unordered_set>
using namespace std;
int a[10005], b[10005];
int main(){
int n, m, k, p, v;
cin >> n >> m;
for(int i=0; i<m; i++) cin >> a[i] >> b[i];
cin >> k;
while(k--){
int c=0;
unordered_set<int> st;
cin >> p;
while(p--){
cin >> v;
st.insert(v);
}for(int i=0; i<m; i++){
if(st.count(a[i])||st.count(b[i])) c++;
}if(m-c) cout << "NO\n";
else cout << "YES\n";
}
}
L2-026 小字辈
看到网上有使用带权并查集维护辈分或者并查集压缩路径时记录距离的题解,这里放个比较粗暴的dfs代码。
#include<iostream>
#include<string.h>
#include<unordered_map>
#include<unordered_set>
#include<set>
using namespace std;
unordered_map<int, unordered_set<int>> mp;
int p[100005], b=1;
set<int> ans;
void dfs(int x, int c){
if(mp.count(x)){
for(auto i:mp[x]){
p[i] = c;
dfs(i, c+1);
}
}
}
int main(){
int n, a;
cin >> n;
memset(p, 0, n+1);
for(int i=1; i<=n; i++){
p[i] = i;
cin >> a;
mp[a].insert(i);
}dfs(-1, 1);
for(int i=1; i<=n; i++){
if(p[i]>b){
ans.clear();
ans.insert(i);
b = p[i];
}else if(p[i]==b) ans.insert(i);
}cout << b << "\n" << *ans.begin();
ans.erase(ans.begin());
for(auto i:ans) cout << ' ' << i;
}