一 理论知识
1 定义: 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)
的合并及查询问题。
2 主要操作:
初始化: 把每个点所在集合初始化为其自身。通常来说,这个步骤在每次
使用该数据结构时只需要一次,无论何种实现方式,时间复杂度
均为 O(N)。
查找: 查找元素所在的集合,即根节点。
合并: 将两个元素所在的集合合并为一个集合。
3 简单实现:
int par[MAX_N], rank[MAX_N];
//加了一个rank数组记录树的高度
//避免像二叉搜索树退化成单链表
//注意在题目数据过大时,可以不
//记录树的高度,否则会超时。
void Init(int n){
int i;
for(i=0; i<n; i++){
par[i]=i;
rank[i]=0;
}
}
int Find(int x){ //注意体会在递归的过程中进行路径压缩
if(x!=par[x])
par[x]=Find(par[x]);
return par[x];
}
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]++;
}
}
二 简单应用
1. 家族关系(最简单的应用)
描述
若某个家族人员过于庞大,要判断两个是否是亲戚,确实
还很不容易,现在给出某个亲戚关系图,求任意给出的两个人
是否具有亲戚关系。 规定:x和y是亲戚,y和z是亲戚,那么x
和z也是亲戚。如果x,y是亲戚,那么x的亲戚 都是y的亲戚,
y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),
分别表示有n个人,m个亲戚关系,询问p对亲戚关系。以下m行:
每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”
或“不具有”亲戚关系。
输入样例:
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出样例:
Yes
Yes
No
参考代码:
#include<stdio.h>
#define MAX_N 5555
int par[MAX_N],rank[MAX_N];
void Init(int n){
int i;
for(i=0; i<n; i++){
par[i]=i;
rank[i]=0;
}
}
int Find(int x){
if(x!=par[x])
par[x]=Find(par[x]);
return par[x];
}
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 j,N,m,p,a,b;
bool Is_r[MAX_N];
scanf("%d %d %d", &N, &m, &p);
Init(N);
for(j=0; j<m; j++){
scanf("%d %d", &a, &b);
Unite(a,b);
}
for(j=0; j<p; j++){
scanf("%d %d", &a, &b);
Is_r[j]=same(a, b);
}
for(j=0; j<p; j++){
if(Is_r[j])
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
2. 食物链 (POJ 1182)
Description
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假 话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
Input
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
Sample Input
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
Sample Output
3
解题思路:
并查集是维护”属于同一组“的数据结构,但是本题中,并不只有属于同一类的信息,还有捕食关系的存在。
对于每只动物 i 创建3个元素 i-A , i-B, i-C, 并用这3*N个元素建立并查集。这个并查集维护如下信息:
1) i-X表示” i属于种类X“.
2) 并查集里的每一个组表示组内所有元素代表的情况都同时发生或不发生。
例如: 如果i-A 和 j-B在同一个组里,就表示如果i属于种类A那么j一定属于种类B,如果j属于种类B
那么i一定属于种类A。因此,对每一条信息可以进行如下操作:
第一种, x 和 y属于同一类, 合并 x-A 和 y-A, 合并 x-B 和 y-B, 合并 x-C 和 y-C。
第二种, x吃y, 合并 x-A 和 y-B, 合并 x-B 和 y-C, 合并 x-C 和 y-A。
再者,在合并之前需要先判断合并是否会产生矛盾。
参考代码:
#include<stdio.h> #define MAX_N 50005 int par[MAX_N*3],rank[MAX_N*3]; int T[MAX_N<<1], X[MAX_N<<1],Y[MAX_N<<1]; void Init(int n){ int i; for(i=0; i<n; i++){ par[i]=i; rank[i]=0; } } int Find(int x){ if(x!=par[x]) par[x]=Find(par[x]); return par[x]; } 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 j,N,K,p,a,b,ans; scanf("%d %d ", &N, &K); Init(N*3); ans=0; for(j=0; j<K; j++){ scanf("%d %d %d", &p, &a, &b); a=a-1; b=b-1; //不正确的编号 if(a<0||a>=N||b<0||b>=N){ ans++; continue; } if(p==1){ //“X和y属于同一类”的信息 if(same(a, b+N)||same(a,b+2*N)){ ans++; } else{ unite(a, b); unite(a+N, b+N); unite(a+2*N, b+2*N); } } else{ //“x吃y”的信息 if(same(a, b)||same(a, b+2*N)){ ans++; } else{ unite(a, b+N); unite(a+N, b+2*N); unite(a+2*N, b); } } } printf("%d\n", ans); return 0; }