并查集
维护一些不相交的集合,它是一个集合的集合。每个元素恰好属于一个集合,好比每条鱼装在一个鱼缸里。每个集合S有一个元素作为\集合代表"rep[S],好比每个鱼缸选出一条"鱼王"。并查集提供三种操作:
MakeSet(x):建立一个新集合x。x应该不在现有的任何一个集合中出现。
Find(S, x):返回x所在集合的代表元素。
Union(x, y):把x所在的集合和y所在的集合合并。
森林表示法
可以用一棵森林表示并查集,森林里的每棵树表示一个集合,树根就是集合的代表元素。一个集合还可以用很多种树表示,只要树中的结点不变,表示的都是同一个集合。
合并操作
只需要将一棵树的根设为另一棵即可。这一步显然是常数的。一个优化是:把小的树合并到大树中,这样会让深度不太大。这个优化称为启发式合并.
查找操作
只需要不断的找父亲,根就是集合代表。一个优化是把沿途上所有结点的父亲改成根。这一步是顺便的,不增加时间复杂度,却使得今后的操作比较快。这个优化称为路径压缩。
下面是最基本得并查集代码,牢记,用得多了,写这种常用数据结构应该做到倒背如流,才可应对ACM中出现的各种变形 !!!
const int SIZE = 100;
int p[SIZE],rank_deep[SIZE];
//rank_deep[]记录该集合的深度//可以根据题目设置不同意义的rank数组
void makeset(int x){
p[x] = x;
rank_deep[x] = 0;
}
//非递归的方式进行路径压缩,更加直观一些
int findSet(int x){
int px = x,i;
//find root
while(px!=p[px])px = p[px];
//path compression
while(x != px){
i=p[x];
p[x]=px;
x=i;
}
return px;
}
void unionSet(int x,int y){
x = findSet(x);
y = findSet(y);
//原则:小的接到大的后面
if(rank_deep[x]>rank_deep[y])
p[y]=x;
else{
p[x]=y;
//deep的定义为多分枝的最远那一条
if(rank_deep[x]==rank_deep[y])rank_deep[y]++;
}
}
实际应用:
POJ-1611 http://poj.org/problem?id=1611
/*并查集的基本应用——POJ1611*/
/************************************************************************
大致题意:一共有n个学生(编号0 至 n-1),m个组,一个学生可以同时加入不同的组。
现在有一种传染病,如果一个学生被感染,那么和他同组的学生都会被感染。现在已
知0号学生被感染,问一共有多少个人被感染。
首先将每个学生都初始化为一个集合,然后将同组的学生合并,设置一个数组num[]
来记录每个集合中元素的个数,最后只要输出0号学生所在集合中元素的个数即可。
**************************************************************************/
#include<iostream>
usingnamespace std;
//num[]数组存储节点所在的集合的总个数
//father[]数组存储dina
int num[30001],father[30001];
//初始化用,把每一个点定义为一个集合
void makeSet(int x){
father[x]=x;//定义跟节点的标志:即为与父亲数组的值相同
num[x]=1;
}
/**********************************************************/
//查找x元素所在的集合,返回根节点
//并且采用递归方式压缩路径,使得每一个点都指向根节点
int findSet(int x){
if(x!=father[x])//只有根节点的父亲节点才与自己的值相同
x=findSet(father[x]);
return x;//此时的x已经被层层修改为最根的那个节点
}
void unionSet(int a,int b){
a = findSet(a);
b = findSet(b);
if(a==b)return;//在同一个集合中,直接退出
//此if,else语句将小集合合并到大集合中
//用来平衡树的左右形状,减少整体层数
if(num[a]<=num[b]){
father[a]=b;
num[b]+=num[a];//更新集合的个数
}
else{
father[b]=a;
num[a]+=num[b];
}
}
int main(){
int m,n;
while(cin>>m>>n&&(m!=0||n!=0)){
for(int i=0;i<m;i++)makeSet(i);
int t,first,next;
while(n--){
cin>>t>>first;
for(int j=1;j<t;j++){
cin>>next;
unionSet(first,next);
}
}
cout<<num[findSet(0)]<<endl;
}
return0;
}
POJ 2492 http://poj.org/problem?id=2492
/************************************************************/
//并查集的应用推广//POJ2492
/************************************************************/
#include<iostream>
usingnamespace std;
// r[i] 0代表r[i]与i同性, 1代表i与r[i]异性
int n,p[2002],r[2002];
/************************************************************/
int find(int x){
int temp=p[x];
if(x==p[x])return x;
p[x]=find(p[x]);
//首先要明白一点,程序已经从递归中跳出,p[x]已经修改到
//根节点,也就是说x已经指向祖先,所以要寻找x与祖先的关系
//关系修改的解释:根据递归的堆栈原理,可知,x的父亲与
//祖先的关系(即r[temp])已经清楚(后进先出),而x与
//父亲的关系也是知道的,那么类似于“关系传递”,便可分
//析出x与祖先的关系
r[x]=(r[temp]==r[x]?0:1);
return p[x];
}
/************************************************************/
void make(){//初始化
for(int i = 0;i<=n;i++){
p[i]=i;
r[i]=0;
}
}
/************************************************************/
void unionSet(int x,int y,int px,int py){
p[py]=px;
//关系修改解释:
//有一点要清楚,由题意知,x与y是异性的,那么
//x与x父亲的关系知道,y与与父亲的关系也知道
//求x父亲与y父亲关系便显而易见了
r[py]=r[x]==r[y] ? 1 : 0;
}
/************************************************************/
int main(){
int i,j,m,a,b,px,py,flag,count=1,t;
scanf("%d",&t);
while(t--){
flag=0;
scanf("%d%d",&n,&m);
make();
while(m--){
scanf("%d%d",&a,&b);//由题意知,每输如的一对肯定是异性
px=find(a);
py=find(b);
//如果px==py说明在前面的论断中x与y已经建立起了练习
//而现在要通过r[a]是否等于r[b]来验证是否产生了矛盾
if(px==py&&r[a]==r[b])
flag=1;
else
unionSet(a,b,px,py);
}
if(flag==1)
printf("Scenario #%d:\nSuspicious bugs found!\n",count++);
else
printf("Scenario #%d:\nNo suspicious bugs found!\n",count++);
printf("\n");
}
return0;
}
/**********************************************************/
POJ 1182http://poj.org/problem?id=1182
/********************************************************
此道题目 前天看的时候一点头绪都没有,看了他人的解题报告后
也几乎看不懂,但是首先做了两道并查集的基础题目POJ1611,与
POJ2524,熟悉并查集的结构,又做了两道并查集的拓展题目
POJ2492与1703,,在充分了解并可以熟悉运用并查集后,此题
便可迎刃而解了,也通过此题发现了自学的诀窍“循序渐进”,
刚刚AC掉食物链问题,有些小激动,遂发此感慨,呵呵~~~
********************************************************/
#include<iostream>
usingnamespace std;
int N,K,relation,a,b,count;
int p[50010],r[50010];//p[]为父亲数组,
//r[]为x与p[x]的关系数组
//0代表同类,1代表x吃父亲p[x]
//1代表父亲p[x]吃x
/*******************************************************/
//初始化函数
void make(){
for(int i=1;i<=N;i++){
p[i]=i;
r[i]=0;
}
}
/******************************************************/
//查找根节点(祖宗)的函数,采用递推压缩路径
//使得每一个节点都指向祖宗
int find(int x){
int temp=p[x];//p[x]之后要被修改,先记下x的父亲值
if(x==p[x])return p[x];
p[x]=find(p[x]);//递推压缩路径,都指向了根节点
r[x]=(r[x]+r[temp])%3;//关系修改
return p[x];
}
/*******************************************************/
//合并
void unionSet(int x,int y,int px,int py){
p[px]=py;
r[px]=(r[y]-r[x]+3+relation-1)%3;
//关于三处的关系修改于判断,本人一开始采用枚举的方式,
//不过如果枚举的话,每次使用倒要if(3*3=)九次,很是
//啰嗦,之后总结出公式,不过可以用进行严格向量法证明
/*
if(relation==1){
//r[px]=(r[y]-r[x])%3;
if(r[x]==r[y]){r[px]=0;return;}
if(r[x]==0){r[px]=r[y];return;}
if(r[y]==0){r[px]=r[x];return;}
if(r[x]==1){r[px]=1;return;}
if(r[y]==1){r[px]=2;return;}
}
if(relation==2){
//r[px]=(r[y]-r[x]+1)%3;
if(r[x]==0&&r[y]==0){}
if(r[x]==0&&r[y]==1){}
if(r[x]==0&&r[y]==2){}
if(r[x]==0&&r[y]==)
*/
}
/*******************************************************/
int main(){
cin>>N>>K;
int pa,pb;
count=0;
make();
while(K--){
scanf("\n%d%d%d",&relation,&a,&b);
pa=find(a);
pb=find(b);
//输入数据有矛盾可以直接判断
if(a>N||b>N||(relation==2&&a==b)){count++;continue;}
//pa!=pb,则说明,a与b还未建立关系,所以肯定不会
//产生矛盾
if(pa!=pb){unionSet(a,b,pa,pb);continue;}
//如果产生关系,但是根据已存在的r[a]与r[b]算出的
//a与b之间的关系与输入的relation不相符,则矛盾
if((r[a]-r[b]+3)%3!=(relation-1)){count++;continue;}
}
cout<<count<<endl;
}