并查集的基本操作:
1.find,查询一个元素属于哪一个集合。
2.merge 把两个集合合并成一个大集合,一般不用写一个额外的函数,直接写主函数内就可以了。
在并查集中,我们采用“待元法”,即为每个集合选择一个固定的元素,作为整个集合的“代表”。使用一个树形结构存储每个集合,书上每个结点都是一个元素,树根是集合的代表元素。
poj 2524
一个基础的例题。
题意:多组输入,处理到文件末尾,每组输入一个n、m,表示有n个同学,m个关系,每个关系输入a、b,表示a、b是相同的宗教,问有多少个不同的宗教。
思路:合并每个a、b的集合,最后遍历1 ~ n,有多少个不同的集合就有多少个不同的宗教。fa[i]的初始值可以是i也可以是-1或者0,赋值为0有时可以偷懒,赋值-1和0是为了可以直接用memset初始化,但是容易出错,出错的一个原因是数据输入中有重边。这里还用到了路径压缩和按秩合并。
原理:
1.路径压缩,每次访问find的操作的同时,把访问过的每个结点直接指向树根,这样下次查找会更快,同时可以避免某条路径特别长。
2.按秩合并,也称“启发式合并”,“秩”定义为树的深度,把“秩”较小的树根作为“秩”较大的树根的子结点,这样就是真正的菊花图了,路径合并还是会出现一条路径比较长的情况(这条路径一直没查询到)。数据1e7的时候用会超内存,1e5的时候最佳,小于1e5几乎没有效果。
3.单独用1或者2,复杂度 (log n),两个都用复杂度就是反阿克曼函数级别的了,是一个比对数函数增长还慢的函数。
COde:
#include<iostream> using namespace std; int fa[500005],ran[500005]; inline void init(int n){ for(int i=1;i<=n;i++){ fa[i]=i; ran[i]=1; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline void merge(int i,int j){ int x=find(i),y=find(j); if(ran[x]<=ran[y]) fa[x]=y; else fa[y]=x; if(ran[x]==ran[y]&&x!=y) ran[y]++; } int main(){ int n,m,a,b,i,j=1; while(cin>>n>>m&&(n||m)){ init(n); for(i=1;i<=m;i++){ cin>>a>>b; merge(a,b); } int ans=0; for(i=1;i<=n;i++) if(fa[i]==i) ans++; cout<<"Case "<<j++<<": "<<ans<<endl; } }
poj 1611
题意:
多组输入,每组输入一个n、m,表示n个学生编号0~n-1,分为m组,每组开头一个数字k表示该组有k个同学,问编号为0的同学的那组有多少人,输入以n=0,m=0结束。
思路:
直接合并,让0始终为根,因为数据较小,不需要按秩合并,效果不大。
Code:
#include<iostream> using namespace std; int fa[500005]; inline void init(int n){ for(int i=0;i<=n-1;i++){ fa[i]=i; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline void merge(int i,int j){ int x=find(i),y=find(j); if(x==0) fa[y]=x; else fa[x]=y; } int main(){ int n,m,a,b,k,i; while(cin>>n>>m&&(n||m)){ init(n); while(m--){ cin>>k; for(i=1;i<=k;i++){ if(i!=1){ cin>>b; merge(a,b); a=b; continue; } cin>>a; } } int ans=0; for(i=0;i<=n-1;i++) if(find(i)==0) ans++; cout<<ans<<endl; } }
poj 1703
题意:T组数据,每组数据输入n、m,n,n个罪犯编号1 ~ n,m组操作,D表示A、B不是一个集合的,A表示询问你A、B是一个集合还是不是一个集合又或是还不确定。
思路:比较有趣,并查集擅长动态维护许多具有传递性的关系,等于是连续性的,不等于不是连续性的,因为A、B的关系只有两种,所以A、B不是一个集合可以转化为A和B的对头是一个集合的、B和A的对头是一个集合的,开两倍的数组进行维护,那么就有传递性了。
Code:
#include<iostream> #include<cstdio> using namespace std; #define N 100005 int fa[2*N],ran[2*N]; inline void init(){ for(int i=1;i<=2*N;i++){ fa[i]=i; ran[i]=1; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline void merge(int i,int j){ int x=find(i),y=find(j); if(x==y) return; if(ran[x]<=ran[y]) fa[x]=y; else fa[y]=x; if(ran[x]==ran[y]) ran[y]++; } int main(){ int t,n,m,a,b,i; char ch; scanf("%d",&t); while(t--){ scanf("%d%d",&n,&m); init(); while(m--){ scanf(" %c%d%d",&ch,&a,&b); if(ch=='D'){ merge(a,b+N); merge(a+N,b); } else{ if(find(a)==find(b)) printf("In the same gang.\n"); else if(find(a)==find(b+N)) printf("In different gangs.\n"); else{ puts("Not sure yet."); } } } } }
poj 2236
题意:给你N台电脑,从1-N。一个数字,表示两台计算机的最大通信距离,超过这个距离就无法进行通信。然后分别告诉这些电脑的坐标,接下来有两种操作,第一种O表示这点电脑修好,第二种S,表示测试这两台电脑能不能进行正常的通信.
思路:存储修复的电脑,每修复一个电脑就判断能否和前面的电脑合并。
思路:
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; #define N 1001 int fa[N],ran[N],a[1001],b[1001],em[1001]; inline void init(int x){ for(int i=1;i<=x;i++){ fa[i]=i; ran[i]=1; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline void merge(int i,int j){ int x=find(i),y=find(j); if(x==y) return; if(ran[x]<=ran[y]) fa[x]=y; else fa[y]=x; if(ran[x]==ran[y]) ran[y]++; } int check(int x,int y,int a,int b,int d){ int temp=(x-a)*(x-a)+(y-b)*(y-b); if(temp>d*d) return 0; return 1; } int main(){ int n,d,i,aa,bb,m,j=0; char ch; scanf("%d%d",&n,&d); init(n); for(int i=1;i<=n;i++) scanf("%d%d",&a[i],&b[i]); while(~scanf(" %c",&ch)){ if(ch=='O'){ scanf("%d",&m); for(i=1;i<=j;i++){ int temp=em[i]; if(check(a[temp],b[temp],a[m],b[m],d)) merge(temp,m); } em[++j]=m; } else{ scanf("%d%d",&aa,&bb); if(find(aa)==find(bb)) printf("SUCCESS\n"); else printf("FAIL\n"); } } }
poj 2492
题意:给出N条虫子,然后a和b交配,给出M对a和b后问有没有同性恋的虫子(题目意思太BT了)。
思路:和poj 1703一样的思路,只是如果发现a、b是同性(也就是一个集合)的,后面就不用判断,输出有同性恋就可以了。
Code:
#include<iostream> #include<cstdio> using namespace std; #define N 2123 int fa[2*N],ran[2*N]; inline void init(){ for(int i=0;i<=2*N;i++){ fa[i]=i; ran[i]=1; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline void merge(int i,int j){ int x=find(i),y=find(j); if(x==y) return; if(ran[x]<=ran[y]) fa[x]=y; else fa[y]=x; if(ran[x]==ran[y]) ran[y]++; } int main(){ int t,n,m,a,b,i,j=1; scanf("%d",&t); while(t--){ int flag=1; scanf("%d%d",&n,&m); init(); while(m--){ if(flag){ scanf("%d%d",&a,&b); int x=find(a),y=find(b); if(x==y) flag=0; else{ merge(a,b+N); merge(a+N,b); } } else scanf("%*d%*d"); } if(j>1) printf("\n"); printf("Scenario #%d:\n",j++); if(!flag) printf("Suspicious bugs found!\n"); else printf("No suspicious bugs found!\n"); } return 0; }
poj 1988
题意:有n个元素,开始每个元素自己一栈,有两种操作,将含有元素x的栈放在含有y的栈的顶端,合并为一个栈。第二种操作是询问含有x元素下面有多少个元素。
思路:将含有元素x的栈的父结点设为含有y的栈,用ran[x]记录此时x下面的元素个数,size[y]表示此时含有y的栈的元数个数。
Code:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; int fa[30005],ran[30005],size[30005]; inline void init(){ for(int i=1;i<=30005;i++){ fa[i]=i; ran[i]=0; size[i]=1; } } int find(int x){ if(x==fa[x]) return x; else{ int t=fa[x]; fa[x]=find(fa[x]); ran[x]+=ran[t]; return fa[x]; } } inline void merge(int i,int j){ int x=find(i),y=find(j); fa[x]=y; ran[x]+=size[y]; size[y]+=size[x]; } int main(){ int t,a,b; char ch; init(); scanf("%d",&t); while(t--){ scanf(" %c",&ch); if(ch=='M'){ scanf("%d%d",&a,&b); merge(a,b); } else{ scanf("%d",&a); find(a); printf("%d\n",ran[a]); } } }
poj 1182
题意:根据动物之间的吃与被吃关系通过并查集把他们连起来,输出语句出现矛盾的次数,默认先输入的正确(此矛盾包括:1、自身矛盾,2、与N矛盾)。(并查集的经典题)
思路:出处 别人的博客。非常详细。
如果两个数在同一个集合(在这里同一个集合表示两者关系已经确定了)里或者自身矛盾已经与N矛盾,直接判断真假,没出现过就把他们合并到一个区间。
合并树时要注意顺序,我这里是把 x 树的根当做主根。找父亲节点时,要不断更新 r[]的值。这里有两个关系:如果 x 和y 为关系 r1, y 和 z 为关系 r2 ,那么 x 和z的关系就是 (r1+r2)%3。
x和fx的关系是ran[x],y和fy的关系是ran[y],y和x的关系是d-1(d=0表示相等,d=1,d=1表示a被b吃,d=2表示a吃b),那么fy 对 fx 的关系是(3-r[y] + d-1 + r[x])%3。
Code:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define N 50000 int fa[N],ran[N],n; inline void init(){ for(int i=1;i<=n;i++){ fa[i]=i; ran[i]=0; } } int find(int x){ if(x==fa[x]) return x; int t=fa[x]; fa[x]=find(fa[x]); ran[x]=(ran[x]+ran[t])%3; return fa[x]; } inline void merge(int i,int j,int d){ int x=find(i),y=find(j); if(x==y) return; fa[y]=x; ran[y]=(3-ran[j]+d-1+ran[i])%3; } int main(){ int m,a,b,d,i,ans=0; scanf("%d%d",&n,&m); init(); while(m--){ scanf("%d%d%d",&d,&a,&b); if(a>n||b>n||(d==2&&a==b)) ans++; else if(find(a)==find(b)){ if(d==1&&ran[a]!=ran[b]) ans++; if(d==2&&(ran[a]+1)%3!=ran[b]) ans++; } else merge(a,b,d); } printf("%d\n",ans); }
hdu 3635
题意:起初球i是被放在i号城市的,在年代更迭,世事变迁的情况下,球被转移了,而且转移的时候,连带该城市的所有球都被移动了:T A B(A球所在的城市的所有球都被移动到了B球所在的城市),Q A(问:A球在那城市?A球所在城市有多少个球呢?A球被转移了多少次呢?)。
思路;一个数组size[x]记录以x为代表的集合的元数个数,ran[x]则记录x被搬运地方次数。
Code:
#include<iostream> #include<cstdio> using namespace std; int fa[10005],ran[10005],size[10005]; inline void init(int n){ for(int i=1;i<=n;i++){ fa[i]=i; ran[i]=0; size[i]=1; } } int find(int x){ if(x==fa[x]) return x; else{ int t=fa[x]; fa[x]=find(fa[x]); ran[x]+=ran[t]; return fa[x]; } } inline void merge(int i,int j){ int x=find(i),y=find(j); fa[x]=y; size[y]+=size[x]; ran[x]++; } int main(){ int t,n,q,j=1; char ch; scanf("%d",&t); while(t--){ scanf("%d%d",&n,&q); printf("Case %d:\n",j++); init(n); while(q--){ int a,b; scanf(" %c",&ch); if(ch=='T'){ scanf("%d%d",&a,&b); merge(a,b); } else{ scanf("%d",&a); int ans=find(a); printf("%d %d %d\n",ans,size[ans],ran[a]); } } } return 0; }
hdu 1856
题意:通俗点讲就是有几个人,他们每个人有自己的小团体,要你求他们中人数最多的数量
思路:离散化后可以按秩合并,没有离散化就不能按秩合并,size[x]记录这个集合的元素个数,每次合并时记录最大值就可以了。
Code:
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int fa[10000005],size[10000005]; inline void init(){ for(int i=1;i<=10000000;i++){ fa[i]=i; size[i]=1; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline int merge(int i,int j){ int x=find(i),y=find(j); if(x!=y){ fa[x]=y; return size[y]+=size[x]; } else return size[y]; } int main(){ int n,a,b,i; while(~scanf("%d",&n)){ if(n==0){ printf("1\n"); continue; } int ans=0; init(); for(i=1;i<=n;i++){ scanf("%d%d",&a,&b); ans=max(ans,merge(a,b)); } printf("%d\n",ans); } }
hdu 1272
题意:给不知道多少对无向边,构成一条边,问是否联通且无环。(重边也是输出no)
思路:合并时判断有无成环,如果没有成环就判断所有的边是否联通。
Code:
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int fa[100005],ran[100005],vis[100005]; inline void init(){ for(int i=1;i<=100000;i++){ fa[i]=i; ran[i]=1; vis[i]=0; } } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } inline int merge(int i,int j){ int x=find(i),y=find(j); if(x==y) return 0; fa[x]=y; return 1; } int check(){ int i,x,flag=0; for(i=1;i<=100005;i++) if(vis[i]){ if(flag){ if(find(i)!=x){ return 0; } } else{ x=find(i); flag=1; } } return 1; } int main(){ int a=0,b=0,i; while(a!=-1){ int flag=1; init(); while(scanf("%d%d",&a,&b),a&&a!=-1){ vis[a]=vis[b]=1; if(flag) flag=merge(a,b); } if(flag==0) printf("No\n"); else if(a!=-1){ if(check()) printf("Yes\n"); else printf("No\n"); } } }
hdu 1325
题意:和1272一样的输入,但是这里是有向边,第一个整数指向第二个整数。
思路:不能成环,全部联通,任何一个节点的入度不能大于1,重边不要紧。
Code:
#include<stdio.h> #include<string.h> #define N 11000 int fa[N],vis[N],maxn; void init(){ for(int i=1;i<N;i++) fa[i]=i; memset(vis,0,sizeof(vis)); maxn=0; } int find(int x){ return x==fa[x]?x:(fa[x]=find(fa[x])); } int merge(int i,int j,int flag){ int x=find(i),y=find(j); if(x==y||y!=j) return 0;//入度不为1 fa[y]=x; return flag; } int max(int a,int b){ return a>b?a:b; } int main(){ int a,b,j=1,flag=1; init(); while(~scanf("%d%d",&a,&b)&&a!=-1){ if(!a){ int ans=0; for(int i=1;i<=maxn;i++) if(vis[i]&&i==fa[i]) ans++; if(ans<=1&&flag) printf("Case %d is a tree.\n",j++); else printf("Case %d is not a tree.\n",j++); init(); flag=1; continue; } vis[a]=vis[b]=1; maxn=max(maxn,max(a,b)); flag=merge(a,b,flag); } }
hdu 1198
题意:A ~ K 代表n个管道图,给出一个m * n的二维字符数组,判断当整个图都能流到水时需要放几口井。
思路:判断每个点和它右边、下边是否可以合并,可以就合并,最后判断有几个联通块就可以了。
Code:
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #include<queue> #include <vector> #include<map> #include<set> #include<utility> #define js ios::sync_with_stdio(false) using namespace std; int fa[2510]; char mapp[55][55]; int find(int x) { return fa[x]==-1?x:(fa[x]=find(fa[x])); } int main() { int n,m; while(scanf("%d%d",&n,&m),n+1) { memset(fa,-1,sizeof fa); for(int i=0;i<n;++i) scanf("%s",mapp[i]); for(int i=0;i<n;++i) for(int j=0;j<m;++j) { if(j!=m-1) { if(mapp[i][j]=='B'||mapp[i][j]=='D'||mapp[i][j]=='F'||mapp[i][j]=='G'||mapp[i][j]=='I'||mapp[i][j]=='J'||mapp[i][j]=='K') if(mapp[i][j+1]=='A'||mapp[i][j+1]=='C'||mapp[i][j+1]=='F'||mapp[i][j+1]=='G'||mapp[i][j+1]=='H'||mapp[i][j+1]=='I'||mapp[i][j+1]=='K') { int x=find(i*m+j+1),y=find(i*m+j+2); if(x!=y) fa[y]=x; } } if(i!=n-1) { if(mapp[i][j]=='C'||mapp[i][j]=='D'||mapp[i][j]=='E'||mapp[i][j]=='H'||mapp[i][j]=='I'||mapp[i][j]=='J'||mapp[i][j]=='K') if(mapp[i+1][j]=='A'||mapp[i+1][j]=='B'||mapp[i+1][j]=='E'||mapp[i+1][j]=='G'||mapp[i+1][j]=='H'||mapp[i+1][j]=='J'||mapp[i+1][j]=='K') { int x=find(i*m+j+1),y=find(i*m+j+m+1); if(x!=y) fa[y]=x; } } } int ans=0; for(int i=1;i<=n*m;++i) if(find(i)==i) ans++; printf("%d\n",ans); } }
hdu 2586
题意:一个村子里有n个房子,这n个房子用n-1条路连接起来,接下了有m次询问,每次询问两个房子a,b之间的距离是多少。
思路:最近公共祖先或者并查集+深搜。因为数据弱,我并查集的思路很简单,不去路径合并(其实路径合并也可以),size[x]=d,表示x到父结点的距离是d,询问的时候算出每个结点到根的距离,然后相减就可以了。
Code:
LCA代码戳我
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #include<queue> #include <vector> #include<map> #include<set> #include<utility> #define js ios::sync_with_stdio(false) using namespace std; typedef long long ll; int fa[40005],size[40005],ans1=0,ans2=0; inline init(int x){ for(int i=1;i<=x;++i) { fa[i]=i; size[i]=0; } } void merge(int i,int j,int d) { if(size[i]) { fa[j]=i; size[j]=d; } else { fa[i]=j; size[i]=d; } } int find1(int x){ if(fa[x]==x) return ans1; ans1+=size[x]; find1(fa[x]); } int find2(int x){ if(fa[x]==x) return ans2; ans2+=size[x]; find2(fa[x]); } int main() { int t,n,m; js; cin>>t; while(t--) { int a,b,c; cin>>n>>m; init(n); for(int i=1;i<=n-1;++i) { cin>>a>>b>>c; merge(a,b,c); } for(int i=1;i<=m;++i) { cin>>a>>b; ans1=ans2=0; cout<<abs(find1(a)-find2(b))<<endl; } } }
hdu 6109
题意:m条信息由ans组数据组成,每条信息的最后一个整数e=1表示A、B相等,e=0表示A、B不相等,每组数据加上最后一条信息就会矛盾,不加上就不会矛盾,问ans的值,并输出每组输出有几条信息。
思路:用set存出现过的数字,便于找下一组数据之前初始化,再用一个set存结点i的对头,每次合并时将被合并的数的对头移到父亲的对头集合里,再把被合并的数从对头的对头集合里抹掉。如果x、y是相等的,但是不是一个集合或y是x的对头,或x、y不相等,但是x、y是一个集合的,那么这是一组数据的结尾。(理论上是可以开两倍的fa[]数组去写不用set,就和1703的思路差不多,但是这是很久以前写的,我也就没试,江大佬亲自验证是错的)
Code:
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #include<queue> #include <vector> #include<map> #include<set> #include<utility> #define js ios::sync_with_stdio(false) using namespace std; typedef long long ll; int fa[100005],ran[100005],ans[100005]; set<int>st; set<int>s[100005]; inline void init(){ set<int>::iterator it; for(it=st.begin();it!=st.end();++it) { fa[*it]=-1; ran[*it]=1; s[*it].clear(); } st.clear(); } int find(int x) { return fa[x]==-1?x:(fa[x]=find(fa[x])); } void merge(int i,int j) { if(i==j) return; set<int>::iterator it; if(ran[i]==0) ran[i]=1; if(ran[j]==0) ran[j]=1; if(ran[i]>ran[j]) { for(it=s[j].begin();it!=s[j].end();it++) { s[*it].erase(j); s[*it].insert(i); s[i].insert(*it); } fa[j]=i; } else{ for(it=s[i].begin();it!=s[i].end();it++) { s[*it].erase(i); s[*it].insert(j); s[j].insert(*it); } fa[i]=j; } if(ran[i]==ran[j]) ran[j]++; } int main() { int T,a,b,e,x,y,sum=0,jy=0; js; cin>>T; memset(fa,-1,sizeof fa); while(T--) { cin>>a>>b>>e; st.insert(a),st.insert(b); x=find(a),y=find(b); sum++; if(e==1) { if(x==y||s[x].count(y)==0) { merge(x,y); } else { ans[++jy]=sum; sum=0; init(); } } else { if(x==y) { ans[++jy]=sum; sum=0; init(); } else { s[x].insert(y); s[y].insert(x); } } } cout<<jy<<endl; for(int i=1;i<=jy;++i) cout<<ans[i]<<endl; return 0; }