并查集是一种高效的数据结构,应用也很广。大一时候简单看了别人的并查集模板,于是生搬硬套过来也A了几道并查集的题目,但是对于其中的一些细节还是不太了解。如今再来看看并查集,有了些新的收获。
初学者推荐一条例题引入并查集。
hdu 1232畅通工程 http://acm.hdu.edu.cn/showproblem.php?pid=1232
首先是初始化根节点par数组和对应的rank数组。如果是简单的题目,rank其实可以省略,但是rank使得并查集算法更高效,且防止了树的退化。
int par[max_n]; int rank[max_n]; void ini(int n) { for(int i=0;i<n;i++) {par[i]=i;rank[i]=0;} }
rank[i]存储了i节点的深度作为附加信息。
先介绍第一种find()
int find(int x) { int r=x; while(par[r] !=r) r=par[r]; return r; }这是最直观的find(),目的只有一个,即为找到父亲节点。
第二种find()
int find(int i) { if (par[i] != i) { par[i] = find(par[i]); } return par[i]; }
递归实现,这个方法就是常说的路径压缩,在找寻父亲节点的过程中更新了par[],减少了复杂度。但是递归实现始终有一个通病:容易栈溢出。RE的一般问题都是出现在这儿。
第三种find()
int find(int x) { int r=x; while(r!=par[r]) r=par[r]; int i=x,j; while(par[i]!=r) { j=par[i]; par[i]=r; i=j; } return r; }既达到路径压缩功能,又防止了栈溢出。这是up主使用的方法,推荐使用。
下面就是unite()函数
void unite(int x,int y) { x=find(x); y=find(y); if(x==y) return ; if(rank[x]<rank[y]) par[x]=y; else { par[y]=x; if(rank[x]==rank[y]) rank[x]++; } }在unite两个节点x,y时,不仅要做到par的更新,还要更新rank,防止树的退化。
下面方便起见,我定义了一个same函数
bool same(int x,int y) { return find(x)==find(y); }
判断是否属于同一组。
这种数据结构是比较容易实现的,变形也很多。
下面给出hdu1232的代码,就是上述算法的实现:
#include <algorithm> #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> using namespace std; const int max_n=2010; int par[max_n]; int rank[max_n]; int n; void ini(int n) { for(int i=0;i<n;i++) {par[i]=i;rank[i]=0;} } int find(int x) { int r=x; while(r!=par[r]) r=par[r]; int i=x,j; while(par[i]!=r) { j=par[i]; par[i]=r; i=j; } return r; } void unite(int x,int y) { x=find(x); y=find(y); if(x==y) return ; if(rank[x]<rank[y]) par[x]=y; else { par[y]=x; if(rank[x]==rank[y]) rank[x]++; } } bool same(int x,int y) { return find(x)==find(y); } int main() { int N,M; while(scanf("%d",&N)==1) { if(N==0) break; ini(max_n); scanf("%d",&M); int count=N-1;//初始化为N-1条路 while(M--) { int a,b; scanf("%d%d",&a,&b); if(!same(a,b))//每找到两条已经相连的路就少建设一条路 { unite(a,b); count--; } } printf("%d\n",count); } return 0; }一天又过去了,今天过的怎么样?梦想是不是更远了?