并查集
一、并查集定义、基本操作、路径压缩
并查集性质:并查集产生的每一个集合都是一棵树
1、并查集定义及初始化
//用一个数组实现并查集 int father[N]; //初始化 for(int i=1;i<=N;i++) { father[i]=i; }
2、查找
规定同一个集合中只存在一个根结点,因此查找操作即对给定的结点寻找其根结点的过程。实现方式可以是递归或递推
①递推,findFather函数返回元素x所在集合的根结点
int findFather(int x) { while(x!=father[x]){ x=father[x]; //获取自己的父亲结点 } return x; }
②递归
int findFather(int x) { if(x==father[x]) return x; //如果找到根结点,则返回根结点编号x else return findFather(father[x]); //否则递归判断x的父亲结点是否是根结点 }
3、合并
把给定的两个元素所在的集合合并
思路:
①对于给定的两个元素a、b,判断它们是否属于同一个集合,可以调用上面的查找函数,对这两个元素a,b分别查找根结点,然后判断其根结点是否相同。
②合并两个集合:在①中已经获得两个元素的根结点faA和faB,因此只要把其中一个的父亲结点指向另一个结点,例如father[faA]=faB或者father[faB]=faA。
void Union(int a,int b) { int faA=findFather(a); //查找a的根结点,记为faA int faB=findFather(b); //查找b的根结点,记为faB if(faA!=faB){ //不属于同一个集合 father[faA]=faB; //合并它们 } }
4、路径压缩
把当前查询结点的路径上的所有结点的父亲都指向根结点,查询复杂度降为O(1)
思路:
①按原先的写法获得x的根结点r
②重新从x开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲全部改为根结点r
int findFather(int x){ //由于x在下面的while种会变成根结点,因此先把原先的x保存一下 int a=x; while(x!=father[x]){ //寻找根结点 x=father[x]; } //到这里,x存放的是根结点,下面吧路径上的所有结点的father都改为根结点 while(a!=father[a]) { int z=a; //因为a要被father[a]覆盖,所以先保存a的值,以修改father[a] a=father[a]; //a回溯父亲结点 father[z]=x; //将原先的结点a的父亲结点改为根结点x } return x; //返回根结点 }
递归写法
int findFather(int v){ if(v==father[v]) return v; //找到根结点 else{ int F=findFather(father[v]); //递归寻找father[v]的根结点 father[v]=F; //将根结点F赋值给father[v] return F; //返回根结点F } }
二、练习题
HDU 1856
题意:有编号为1到10^7的男生在一个房间,现在给出n组数据,每组数据包括两个编号,且这两个编号的人是朋友,如果A和B是朋友,B和C是朋友,那么A,B,C三人都是朋友,哪一个朋友圈中人数最多,输出这个朋友圈的人数;
思路:并查集思想,多个集合中元素最多的即为所求;可看做求每个集合的元素数目,这里借助num数组,存储以该结点作为根结点时的集合元素总数,初始值均为1,可看做每个元素最初单独构成一个集合,在合并两个不在同一集合的元素时,更新根结点的num值,并比较更新后的num值和maxm,maxm总是取当前各个集合元素的最大值。最后输出maxm即为所求。这里数据量较大,容易时间超限,故用路径压缩;
#include <iostream> #include <cstdio> using namespace std; #define maxn 10000005 int father[maxn],num[maxn],maxm; void Initial() { for(int i=1;i<=maxn;i++){ father[i]=i; num[i]=1; } } int findFather(int x) { int a=x; while(x!=father[x]) { x=father[x]; } //路径压缩 while(a!=father[a]) { int d=a; a=father[a]; father[d]=x; } return x; } void Union(int a,int b) { int faA=findFather(a); int faB=findFather(b); if(faA!=faB){ father[faA]=faB; num[faB]+=num[faA]; maxm=max(maxm,num[faB]); } } int main() { int n,var1,var2; while(cin>>n) { Initial(); maxm=0; if(n==0){ printf("1\n"); continue; } for(int i=0;i<n;i++) { scanf("%d %d",&var1,&var2); Union(var1,var2); } cout<<maxm<<endl; } return 0; }
问题 A: 通信系统
题目描述
为了提高传送效率和节约资源,要求当消息在某个端点生成后,其余各个端点均能接收到消息,并且每个端点均不会重复收到消息。
现给你通信系统的描述,你能判断此系统是否符合以上要求吗?
输入
每组输入首先包含2个整数N和M,N(1<=N<=1000)表示端点个数,M(0<=M<=N*(N-1)/2)表示通信线路个数。
接下来M行每行输入2个整数A和B(1<=A,B<=N),表示端点A和B由一条通信线缆相连。两个端点之间至多由一条线缆直接相连,并且没有将某个端点与其自己相连的线缆。
当N和M都为0时,输入结束。
输出
思路:由于题目的限制,若构成的图形成环则直接输出no即可,而根据并查集的性质,并查集一定会把环拆解,即并查集产生的每一个集合都是树,所以一开始不知道怎么判断产生了环,后来想到如果图中边的数目大于顶点数目减一则该图中一定有环,而题目又声明两个端点之间至多由一条线缆直接相连,并且没有将某个端点与其自己相连的线缆。故可以放心用m>n-1判断图中有环,直接Pass掉。
#include <iostream> #include <cstdio> #include <set> using namespace std; #define maxn 1005 int father[maxn]; void Initial() { for(int i=1;i<=maxn;i++) father[i]=i; } int findFather(int x) { int a=x; while(x!=father[x]) { x=father[x]; } //路径压缩 while(a!=father[a]) { int d=a; a=father[a]; father[d]=x; } return x; } void Union(int a,int b) { int faA=findFather(a); int faB=findFather(b); if(faA!=faB) { father[faA]=faB; } } int main() { int n,m,var1,var2; while(cin>>n>>m) { Initial(); set<int> st; if(n==0&&m==0) break; for(int i=0;i<m;i++) { cin>>var1>>var2; Union(var1,var2); } if(m>n-1){ //表示一定会构成环 cout<<"No\n"; continue; } for(int i=1;i<=n;i++) { findFather(i); st.insert(father[i]); } if(st.size()==1){ cout<<"Yes\n"; }else{ cout<<"No\n"; } } return 0; }
好朋友
#include <cstdio> #include <iostream> #include <set> using namespace std; #define maxn 105 int father[maxn]; void Initial(int father[]) { for(int i=1;i<=maxn;i++) father[i]=i; } int findFather(int x) { int a=x; while(x!=father[x]){ x=father[x]; } while(a!=father[a]) { int z=a; a=father[a]; father[z]=x; } return x; } void Union(int a,int b) { int faA=findFather(a); int faB=findFather(b); if(faA!=faB){ father[faA]=faB; } } int main() { int n,m,num=1; int var1,var2; set<int> st; Initial(father); cin>>n>>m; for(int i=0;i<m;i++) { cin>>var1>>var2; Union(var1,var2); } for(int i=1;i<=n;i++){ findFather(i); st.insert(father[i]); } cout<<st.size()<<endl; return 0; }
问题B:畅通工程
http://codeup.cn/problem.php?cid=100000615&pid=1
#include <cstdio> #include <iostream> #include <set> using namespace std; #define maxn 1005 int father[maxn]; void Initial(int n) { for(int i=1;i<=n;i++) father[i]=i; } int findFather(int x) { int a=x; while(x!=father[x]){ x=father[x]; } while(a!=father[a]) { int z=a; a=father[a]; father[z]=x; } return x; } void Union(int a,int b) { int faA=findFather(a); int faB=findFather(b); if(faA!=faB){ father[faA]=faB; } } int main() { int n,m; int var1,var2; while(cin>>n) { if(n==0) break; cin>>m; set<int> st; Initial(maxn); for(int i=0;i<m;i++) { cin>>var1>>var2; Union(var1,var2); } for(int i=1;i<=n;i++) { findFather(i); st.insert(father[i]); } cout<<st.size()-1<<endl; } return 0; }
问题C:How Many Tables
http://codeup.cn/problem.php?cid=100000615&pid=2
#include <cstdio> #include <iostream> #include <set> using namespace std; #define maxn 1005 int father[maxn]; void Initial(int n) { for(int i=1;i<=n;i++) father[i]=i; } int findFather(int x) { int a=x; while(x!=father[x]){ x=father[x]; } while(a!=father[a]) { int z=a; a=father[a]; father[z]=x; } return x; } void Union(int a,int b) { int faA=findFather(a); int faB=findFather(b); if(faA!=faB){ father[faA]=faB; } } int main() { int n,m,t; int var1,var2; cin>>t; while(t>0) { cin>>n>>m; set<int> st; Initial(maxn); for(int i=0;i<m;i++) { cin>>var1>>var2; Union(var1,var2); } for(int i=1;i<=n;i++) { findFather(i); st.insert(father[i]); } cout<<st.size()<<endl; t--; } return 0; }