l
一种简单的用途广泛的集合.
l
1、Make_Set(x)
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。
2、Find_Set(x)
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先,具体见示意图
3、Union(x,y)
合并两个不相交集合操作很简单:
利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。如图
l
1、Find_Set(x)时
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示;可见,路径压缩方便了以后的查找。
2、Union(x,y)时
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
数据结构——并查集的应用
并查集是一种简单的数据结构,相对于其他数据结构来说,编程难度很小,也很灵活,适当的find函数与Union函数便可以解决很多问题。
- int find(int x){
- if(x==parent[x]) return x;
- return parent[x]=find(parent[x]);
- }
- void Union(int a,int b){
- int pa=find(a);
- int pb=find(b);
- if(pa!=pb) parent[pa]=pb;
- }
并查集的应用:
并和查有关的集合操作
并查集可以用于相关的集合操作,如判定一个无向图是否有环,输出一个无向图的连通分量个数,kruscal最小生成树的操作。一些基于集合,有添加其它性质的集合操作。
例题:
TOJ2469
题目描述:有n个人,m对朋友关系,朋友关系对称且可传递,求有几个朋友圈。
分析:事实上是求一个无向图的连通分量数,并查集轻松搞定。
TOJ3294
题目描述:一开始有n个Block,分别放置在地面上,有P个操作,操作有两种类型:
M
C
分析:定义parent同并查集的一般操作,cnt表示有多少块Block[x]被压在x之下,size[x]表示以x为根的Block组一共有多少个Block.
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define MAX 30030
- int par[MAX],cnt[MAX],size[MAX];
- void init(int n)
- {
- for(int i=0;i<n;i++)
- {
- par[i]=i;
- cnt[i]=0;
- size[i]=1;
- }
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- int tmp=par[x];
- par[x]=find(par[x]);
- cnt[x]+=cnt[tmp];
- return par[x];
- }
- void Union(int a,int b,int pa,int pb)
- {
- par[pa]=pb;
- cnt[pa]+=size[pb];
- size[pb]+=size[pa];
- }
- int main()
- {
- int n,a,b,pa,pb;
- char move[10];
- while(scanf("%d",&n)!=EOF)
- {
- init(MAX);
- for(int i=0;i<n;i++)
- {
- scanf("%s",move);
- if(move[0]=='M')
- {
- scanf("%d%d",&a,&b);
- pa=find(a);
- pb=find(b);
- if(pa!=pb) Union(a,b,pa,pb);
- }
- else if(move[0]=='C')
- {
- scanf("%d",&a);
- find(a);
- printf("%d\n",cnt[a]);
- }
- }
- }
- return 0;
- }
TOJ3732
题目描述:悟空在寻找龙珠,一共有n个龙珠,m条操作。操作有两种。
T
Q
分析:定义parent同并查集的一般操作,step表示经过几步到达现在所在的城,size表示该城里的龙珠数。
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define MAX 10010
- int par[MAX],step[MAX],size[MAX];
- void init(int n)
- {
- for(int i=1;i<=n;i++)
- {
- par[i]=i;
- step[i]=0;
- size[i]=1;
- }
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- int tmp=par[x];
- par[x]=find(tmp);
- step[x]+=step[tmp];
- return par[x];
- }
- void Union(int a,int b)
- {
- int pa=find(a);
- int pb=find(b);
- par[pa]=pb;
- size[pb]+=size[pa];
- step[pa]++;
- }
- int main()
- {
- int T,n,m,a,b,t=1;
- scanf("%d",&T);
- while(T--)
- {
- printf("Case %d:\n",t++);
- scanf("%d%d",&n,&m);
- init(n);
- for(int i=0;i<m;i++)
- {
- char move;
- getchar();
- move=getchar();
- if(move=='T')
- {
- scanf("%d%d",&a,&b);
- Union(a,b);
- }
- else
- {
- scanf("%d",&a);
- int pa=find(a);
- printf("%d %d %d\n",pa,size[pa],step[a]);
- }
- }
- }
- return 0;
- }
种类相关并查集操作
例题:
POJ1182
题目大意:有A,B,C三种动物A吃B,B吃C,C吃A。有两种描述:
1
2
判断有多少句假话(假话题目中有定义,主要是判断是否与之前的描述相悖)
分析:增加属性kind,kind[x]=0,表示与根同类,kind[x]=1,表示吃根,kind[x]=2表示被根吃。
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define MAX 50050
- int par[MAX],rel[MAX];
- void init(int n)
- {
- for(int i=1;i<=n;i++)
- {
- par[i]=i;
- rel[i]=0;
- }
- }
- int find(int x)
- {
- if(par[x]==x) return x;
- int tmp=par[x];
- par[x]=find(tmp);
- rel[x]=(rel[tmp]+rel[x])%3;
- return par[x];
- }
- void union_set(int x,int y,int px,int py,int d)
- {
- par[px]=py;
- rel[px]=(rel[y]-rel[x]+2+d)%3;
- }
- int main()
- {
- int T,n,m,a,b,pa,pb,k,r;
- scanf("%d%d",&n,&m);
- {
- init(n);
- r=0;
- for(int i=0;i<m;i++)
- {
- scanf("%d%d%d",&k,&a,&b);
- if(a>n||b>n) { r++; continue;}
- if(k==2&&a==b) { r++; continue;}
- pa=find(a);
- pb=find(b);
- if(pa==pb)
- {
- if((rel[b]+k+2)%3!=rel[a]) r++;
- }
- else union_set(a,b,pa,pb,k);
- }
- printf("%d\n",r);
- }
- }
TOJ1706
题目大意:给出n个点m条边(无向边),寻找是否有奇环。可用bfs或者dfs黑白染色,用并查集则是顶点种类为2.kind[x]=0表示与根同色,kind[x]=1表示与根异色。
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define MAX 2050
- int par[MAX],rel[MAX];
- void init(int n)
- {
- for(int i=1;i<=n;i++)
- {
- par[i]=i;
- rel[i]=0;
- }
- }
- int find(int x)
- {
- if(par[x]==x) return x;
- int tmp=par[x];
- par[x]=find(tmp);
- rel[x]^=rel[tmp];
- return par[x];
- }
- void union_set(int x,int y,int px,int py)
- {
- par[py]=px;
- rel[py]=(rel[y]==rel[x]);
- }
- int main()
- {
- int T,n,m,a,b,pa,pb,r;
- scanf("%d",&T);
- for(int t=1;t<=T;t++)
- {
- scanf("%d%d",&n,&m);
- init(n);
- r=0;
- for(int i=0;i<m;i++)
- {
- scanf("%d%d",&a,&b);
- if(!r)
- {
- pa=find(a);
- pb=find(b);
- if(pa==pb) r=(rel[a]==rel[b]);
- else union_set(a,b,pa,pb);
- }
- }
- printf("Scenario #%d:\n",t);
- printf("%s bugs found!\n\n",r?"Suspicious":"No suspicious");
- }
- }
POJ1733
题目大意:有长度为n的0,1串,给出描述,a
分析:设属性sum,区间a
- #include <cstdio>
- #include <cstring>
- #include <map>
- #include <algorithm>
- using namespace std;
- #define MAX 5010
- int par[2*MAX],rel[2*MAX],ind[2*MAX];
- struct node
- {
- int s,e;
- bool Isodd;
- }query[MAX];
- map<int ,int>M;
- void init(int n)
- {
- M.clear();
- for(int i=0;i<=n;i++)
- {
- par[i]=i;
- rel[i]=0;
- }
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- int tmp=par[x];
- par[x]=find(tmp);
- rel[x]^=rel[tmp];
- return par[x];
- }
- void union_set(int a,int b,int pa,int pb,int d)
- {
- par[pb]=pa;
- rel[pb]=rel[b]^rel[a]^d;
- }
- int main()
- {
- int n,m;
- scanf("%d%d",&n,&m);
- {
- int a,b,pa,pb,i;
- char str[10];
- init(2*m);
- for(i=0;i<m;i++)
- {
- scanf("%d%d%s",&a,&b,str);
- b++;
- query[i].s=a;
- query[i].e=b;
- query[i].Isodd=(str[0]=='o');
- ind[i<<1]=a;
- ind[i<<1|1]=b;
- }
- sort(ind,ind+2*m);
- for(i=0;i<2*m;i++)
- M[ind[i]]=i+1;
- for(i=0;i<m;i++)
- {
- a=M[query[i].s];
- b=M[query[i].e];
- pa=find(a);
- pb=find(b);
- if(pa==pb)
- {
- if((rel[a]^rel[b])!=query[i].Isodd) break;
- }
- else union_set(a,b,pa,pb,query[i].Isodd);
- }
- printf("%d\n",i);
- }
- return 0;
- }
TOJ3413
题目大意:上题的强化版给出任意的n个数,和m条描述,描述为
分析:设属性sum,a
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define MAX 200010
- int par[MAX],rel[MAX];
- void init(int n)
- {
- for(int i=0;i<=n;i++)
- {
- par[i]=i;
- rel[i]=0;
- }
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- int tmp=par[x];
- par[x]=find(tmp);
- rel[x]+=rel[tmp];
- return par[x];
- }
- void union_set(int a,int b,int pa,int pb,int d)
- {
- par[pb]=pa;
- rel[pb]=rel[a]-rel[b]+d;
- }
- int main()
- {
- int n,m;
- while(scanf("%d%d",&n,&m)!=EOF)
- {
- int a,b,pa,pb,d,r=0;
- init(n);
- for(int i=0;i<m;i++)
- {
- scanf("%d%d%d",&a,&b,&d);
- a--;
- pa=find(a);
- pb=find(b);
- if(pa==pb)
- {
- if(rel[b]-rel[a]!=d) r++;
- }
- else union_set(a,b,pa,pb,d);
- }
- printf("%d\n",r);
- }
- return 0;
- }
用于优化
并查集更多地应用于dp或者贪心中的优化,用来降低复杂度。
例题:
TOJ1681
题目大意:有n件物品,每件物品有两个属性,p和d,表示在第d天之前将该物品卖出可以获得p的收益,假设一天至多能卖一件货物。问最多能获得的收益值。
分析:思想贪心。对货物按收益值进行排序,优先安排出售获益大的商品,对于每件商品可以把它安排在可以安排的最晚的那一天,即d之前的最晚的一天。如果直接暴力,则对于每件货物都需要从d天到第一天进行搜索,找出没有被安排的一天。时间复杂度O(nK),K是d的复杂度。考虑使用并查集优化,定义parent表示在d之前的可用时间(即题目中需要搜索出的时间)。对于每件货物设b=find(d),若b>0则该物品可安排,进行Union(b,b-1)的合并(b应该合并到b-1上),否则该货物无法安排
- #include <cstdio>
- #include <cstring>
- #include <vector>
- #include <algorithm>
- using namespace std;
- #define MAX 10010
- #define MP make_pair
- int par[MAX];
- void init(int n)
- {
- for(int i=0;i<=n;i++)
- par[i]=i;
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- return par[x]=find(par[x]);
- }
- void union_set(int a,int b){ par[a]=b;}
- vector<pair<int,int> > task;
- int main()
- {
- int n,a,b,mx;
- while(scanf("%d",&n)!=EOF)
- {
- task.clear();
- mx=0;
- for(int i=0;i<n;i++)
- {
- scanf("%d%d",&a,&b);
- task.push_back(MP(a,b));
- if(b>mx) mx=b;
- }
- sort(task.begin(),task.end());
- init(mx);
- int ans=0;
- for(int i=n-1;i>=0;i--)
- {
- b=find(task[i].second);
- if(b>0)
- {
- ans+=task[i].first;
- union_set(b,b-1);
- }
- }
- printf("%d\n",ans);
- }
- return 0;
- }
反向进行并查集操作
与并查集不同,给出一个图中原有的一些边,然后给出操作,操作不是向图中添加边,而是在已有的边上,将边删除。对于该种情况,需要把首先读入所有操作,把要求删除的边全部删除,再按照从后往前的顺序处理操作,这样删边操作又重新转化为了添边的操作。
例题:
ZOJ3261
题目大意:有n个卫星,每个卫星有一个power值,初始时这些卫星之间有若干条边,有两种操作一种是删边,另一种是查询,查询卫星a,即要求找出与卫星a直接或间接相连的卫星中power值大于该卫星的拥有最大power值的卫星,若两卫星power值相同且最大输出编号小的那个。
分析:按上述方法,先删边,再反向处理操作,注意该题优先级的描述,再Union中要分类处理。
- #include <cstdio>
- #include <cstring>
- #include <iostream>
- #include <set>
- using namespace std;
- #define MAXN 10010
- #define MAXM 50010
- int query[MAXM][2];
- int par[MAXN],power[MAXN];
- int ans[MAXM];
- set<int> g[MAXN];
- void init(int n)
- {
- for(int i=0;i<n;i++)
- {
- par[i]=i;
- g[i].clear();
- scanf("%d",&power[i]);
- }
- }
- int find(int x)
- {
- if(x==par[x]) return x;
- return par[x]=find(par[x]);
- }
- void Union(int a,int b)
- {
- int pa=find(a);
- int pb=find(b);
- if(power[pa]<power[pb]) par[pa]=pb;
- else if(power[pa]>power[pb]) par[pb]=pa;
- else
- {
- if(pa<pb) par[pb]=pa;
- else par[pa]=pb;
- }
- }
- int main()
- {
- int n,m,q,b=0;
- while(scanf("%d",&n)!=EOF)
- {
- if(b) printf("\n");
- b=1;
- init(n);
- scanf("%d",&m);
- int a,b;
- char str[20];
- for(int i=0;i<m;i++)
- {
- scanf("%d%d",&a,&b);
- if(a>b) swap(a,b);
- g[a].insert(b);
- }
- scanf("%d",&q);
- for(int i=0;i<q;i++)
- {
- scanf("%s",str);
- if(str[0]=='d')
- {
- scanf("%d%d",&a,&b);
- if(a>b) swap(a,b);
- query[i][0]=a; query[i][1]=b;
- g[a].erase(g[a].find(b));
- }
- else
- {
- scanf("%d",&a);
- query[i][0]=a; query[i][1]=-1;
- }
- }
- for(int i=0;i<n;i++)
- {
- for(set<int>::iterator it=g[i].begin();it!=g[i].end();it++)
- Union(i,*it);
- }
- int cnt=0;
- for(int i=q-1;i>=0;i--)
- {
- if(query[i][1]==-1)
- {
- int tmp=find(query[i][0]);
- if(power[tmp]==power[query[i][0]]) ans[cnt++]=-1;
- else ans[cnt++]=tmp;
- }
- else Union(query[i][0],query[i][1]);
- }
- for(int i=cnt-1;i>=0;i--)
- printf("%d\n",ans[i]);
- }
- }