数据结构---并查集小结
By-Missa
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。 (百度百科)
大体分为三个:普通的并查集,带种类的并查集,扩展的并查集(主要是必须指定合并时的父子关系,或者统计一些数据,比如此集合内的元素数目。)
1 #define MAXN 100005 2 int n,m,k,fa[MAXN]; 3 int rank[MAXN]; 4 void init(int n)//初始化 5 { 6 for(int i=0;i<=n;i++) 7 { 8 fa[i]=i; 9 rank[i]=0; 10 } 11 } 12 //查找的时候,进行路径压缩fa[x]=find(fa[x]) 13 //把查找路径上的结点都指向根结点,减少树的高度。 14 int find(int x) 15 { 16 if(x != fa[x]) 17 fa[x]=find(fa[x]);//路径压缩 18 return fa[x]; 19 } 20 //合并 21 void unio(int x,int y) 22 { 23 int fx=find(x),fy=find(y); 24 if(fx==fy) return ; 25 if(rank[fy]<rank[fx])//将rank值小的合并到大的中 26 fa[fy]=fx; 27 else 28 { 29 fa[fx]=fy; 30 if(rank[fx]==rank[fy]) 31 rank[fy]++; 32 } 33 } 34 //或(忽略按秩合并,懒的时候经常这么敲.....时间上也不知道会差多少,没有试过。。): 35 void unio(int x,int y) 36 { 37 int fx=find(x),fy=find(y); 38 if(fx==fy) return ; 39 fa[fy]=fx; 40 }
一.普通并查集:
Poj 1611 ,2524,2236.都是裸的并查集。
简单并查集的一个应用:kruskal需要并查集判断点是否在同一个集合里。
Poj1287
模版最小生成树:
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 using namespace std; 6 #define MAXN 55 7 #define MAXM 10000 8 int fa[MAXN]; 9 int n,m,e,ans; 10 struct Edge 11 { 12 int u; 13 int v; 14 int c; 15 }p[MAXM]; 16 void addEdge(int u,int v,int c) 17 { 18 p[e].v=v;p[e].c=c;p[e].u=u; 19 e++; 20 } 21 void init() 22 { 23 for(int i=0;i<=n;i++) 24 fa[i]=i; 25 } 26 int find(int x)//查找点所在的集合 27 { 28 if(fa[x]!=x) 29 fa[x]=find(fa[x]); 30 return fa[x]; 31 } 32 int cmp(const Edge &a,const Edge & b) 33 { 34 return a.c<b.c; 35 } 36 bool kru(int n,int m) 37 { 38 int i,j; 39 sort(p,p+m,cmp); 40 ans=0; 41 init(); 42 int cnt=0; 43 for(i=0;i<m;i++) 44 { 45 //使用并查集的地方,在每次加入边之前先判断下点是否已经在同 //一个集合了 46 int uu=find(p[i].u); 47 int vv=find(p[i].v); 48 if(uu==vv) 49 continue; 50 fa[uu]=vv; 51 ans+=p[i].c; 52 cnt++; 53 } 54 if(cnt != n-1) 55 return false; 56 else 57 return true; 58 } 59 int main() 60 { 61 while(scanf("%d",&n)) 62 { 63 e=0; 64 if(!n) 65 break; 66 scanf("%d",&m); 67 for(int i=0;i<m;i++) 68 { 69 int a,b,c; 70 scanf("%d%d%d",&a,&b,&c); 71 addEdge(a,b,c); 72 } 73 kru(n,m); 74 printf("%d\n",ans); 75 } 76 return 0; 77 }
二.种类并查集:
最经典的就是 POJ 1182 食物链
题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)
在做这题之前就知道是很经典的并查集了,还是不会做。。。,看了网上很多份解题报告,花了很长的时间来理解这题,下面这份报告的思路http://cavenkaka.iteye.com/blog/1489588 讲的很不错。下面是我根据从网上的解题报告中整理总结的:
思路:
fa[x]表示x的根结点。relat[x]表示fa[x]与x的关系。relat[x] == 0 表示fa[x]与x同类;1表示fa[x]吃x;2表示x吃fa[x]。{relat[]可以抽象成元素i到它的父亲节点的逻辑距离,见下面。}
怎样判断一句话是不是假话?
假设已读入 D , X , Y , 先利用find()函数得到X , Y 所在集合的代表元素 fx,fy ,若它们在同一集合(即 fx== fy )则可以判断这句话的真伪:
1.若 D == 1 (X与Y同类)而 relat[X] != relat[Y] 则此话为假。(D == 1 表示X与Y为同类,而从relat[X] != relat[Y]可以推出 X 与 Y 不同类。比如relat[x]=0 即fx与x同类,而relat[y]=1 即fy吃y,而fx==fy,故矛盾。)
2.若 D == 2 (X吃Y)而 relat[X] == relat[Y] (X 与Y为同类,故矛盾。)或者 relat[X] == ( relat[Y] + 1 ) % 3 (Y吃X )则此话为假。
上个问题中 r[X] == ( r[Y] + 1 ) % 3这个式子怎样推来?
我们来列举一下: 假设有Y吃X(注意fx==fy的前提条件),那么r[X]和r[Y]的值是怎样的?
r[X] = 0 && r[Y] = 2 (X与fx同类,Y吃fy,即Y吃X)
r[X] = 1 && r[Y] = 0 (X被fx吃,Y与fy同类,即Y吃X)
r[X] = 2 && r[Y] = 1 (X吃fx,Y被fy吃,一个环,Y吃X)
通过观察得到r[X] = ( r[Y] + 1 ) % 3;
对于上个问题有更一般的判断方法(来自poj 1182中的Discuss ):
若 ( r[x] - r[y] + 3 ) % 3 != d - 1(d-1 值是1或者0.....) ,则此话为假。
当判断两个元素的关系时,若它们不在同一个集合当中,则它们还没有任何关系,直接将它们按照给出的关系合并就可以了。若它们在同一个集合中,那么它们的关系就是x到y的距离:如图所示为(r[x]+3-r[y])%3,即x与y的已有的关系表达,判断和给出的关系是否一致就可以知道是真话还是假话了。
注意事项:
A、find()函数里面的那句relat[x]=(relat[x] + relat[t])%3解释:
我们用x--r-->y表示x和y之间的关系是r,比如x--1--y代表x吃y。现在,若已知x--r1-->y,y--r2-->z,如何求x--?-->z?
即如何在路径压缩的时候更新x与当前父亲的relat值?
X--r[x]--t(t就是还未压缩的父亲),t---r[t]---root(压缩后的父亲)。
故x---r[x]+r[t]--->root;举例:
r[x]=0;r[t]=1;则x与root的关系是x被root吃。。。。其他类似。
用逻辑距离理解如下(向量思想):
B、当D X Y时,则应合并X的根节点和Y的根节点,同时修改各自的relat。那么问题来了,合并了之后,被合并的根节点的relat值如何变化呢?
现有x和y,d为x和y的关系,fx和fy分别是x和y的根节点,于是我们有x--relat[x]-->fx,y--relat[y]-->fy,显然我们可以得到fx--(3-relat[x])-->x,fy--(3-relat[y])-->y。假如合并后fx为新的树的根节点,那么原先fx树上的节点不需变化,fy树则需改变了,因为relat值为该节点和树根的关系。这里只改变relat(fx)即可,因为在进行find操作时可相应改变fy树的所有节点的relat值。于是问题变成了fx--?-->fy。我们不难发现fx--(3-relat[x])-->x--d-->y--relat[y]-->fy,我们有fx--(3-relat[x])-->x--d-->y--relat[y]-->fy。我们求解了fx和fy的关系。即fx----(relat[y] - relat[x] +3 +d)%3--->fy。(如下图:)
1 //食物链 2 //!!!!!!! 3 #include <iostream> 4 #include <cstdio> 5 #include <cstring> 6 7 using namespace std; 8 9 #define MAXN 50010 10 int N,M,K,fa[MAXN],relat[MAXN];//ralat 表示与父亲的关系,0表示是同类,1表示是x被fa[x]吃,2表示是吃父亲 11 int ans=0; 12 void init(int n) 13 { 14 for(int i=0;i<=n;i++) 15 { 16 fa[i]=i; 17 relat[i]=0; 18 } 19 20 } 21 22 int find(int x) 23 { 24 if( x != fa[x]) 25 { 26 int t=fa[x]; 27 fa[x]=find(fa[x]); 28 relat[x]=(relat[x] + relat[t])%3;//A 29 } 30 return fa[x]; 31 } 32 33 void unio(int x,int y,int d)//d是x,y的关系 34 { 35 int fx=find(x); 36 int fy=find(y); 37 fa[fx]=fy; 38 relat[fx]=(relat[y] - relat[x] +3 +d)%3;//B 39 } 40 41 int main() 42 { 43 int d,x,y; 44 scanf("%d%d",&N,&K); 45 ans=0; 46 init(N); 47 while(K--) 48 { 49 scanf("%d%d%d",&d,&x,&y); 50 if(x>N || y>N) 51 { 52 ans++; 53 continue; 54 } 55 if(d==2 && x==y) 56 { 57 ans++; 58 continue; 59 } 60 int fx=find(x); 61 int fy=find(y); 62 if(fx==fy) 63 { 64 if((relat[x] - relat[y] +3)%3 != d-1)// 65 ans++; 66 } 67 else 68 { 69 unio(x,y,d-1);//d-1==1表示的是x与y的关系 70 } 71 } 72 printf("%d\n",ans); 73 return 0; 74 }
POJ上的种类并查集还有:
POJ-1703、POJ-2492、POJ-1733、POJ-1988等。
POJ-1703 Find them, Catch them(两个互斥集合)
题目大意是:有两个帮派,告诉你那两个人属于不同的帮派,让你判断某两个人得是否在一个帮派中。
并查集的核心是用集合里的一个元素代表整个集合,集合里所有元素都指向这个元素,称它为根元素。集合里任何一个元素都能到达根元素。这一题里,设数组fa[x]表示x的父亲是fa[x](x,fa[x]在一个帮派),diff[x]表示x与diff[x]不在同一个集合里面。
如果是D[b][c]命令的话,即b与c不在同一个帮派,故b与diff[c]在同一个帮派。把b放到diff[c]的集合里,同理把c放到diff[b]里面。
1 if(diff[b] == -1) 2 diff[b]=c; 3 if(diff[c] == -1) 4 diff[c]=b; 5 unio(b,diff[c]); 6 unio(c,diff[b]);
如果是A命令的话,查询b,c的根元素:
1. 根元素相同,b,c在同一个集合里;
2. 根元素不同,但b与diff[c]的根元素相同,b,c不在一个集合里;
3.否则,b,c还没有确定。
POJ-2492与1703基本一样。
另解(更一般的解):这两题可以用食物链的形式写,即简化的食物链,比食物链少一个关系,即相当于1吃2,2吃1。
对应关系即为:
x--(r1+r2)%2->z
fx----(relat[y] - relat[x] +2+d)%2--->fy
下面给出1703的食物链改编版:
1 //食物链改编版 2 //!!!!!!! 3 #include <iostream> 4 #include <cstdio> 5 #include <cstring> 6 using namespace std; 7 #define MAXN 100010 8 int N,M,K,fa[MAXN],relat[MAXN];//此题中0表示在同一类,1表示不在同一类。 9 int ans=0; 10 void init(int n) 11 { 12 for(int i=0;i<=n;i++) 13 { 14 fa[i]=i; 15 relat[i]=0; 16 } 17 } 18 int find(int x) 19 { 20 if( x != fa[x]) 21 { 22 int t=fa[x]; 23 fa[x]=find(fa[x]); 24 relat[x]=(relat[x] + relat[t])%2;//A 25 } 26 return fa[x]; 27 } 28 void unio(int x,int y,int d)//d是x,y的关系 29 { 30 int fx=find(x); 31 int fy=find(y); 32 fa[fx]=fy; 33 relat[fx]=(relat[y] - relat[x] +2 +d)%2;//B 34 } 35 int main() 36 { 37 int x,y; 38 char op; 39 char buf[10]; 40 int t; 41 scanf("%d",&t); 42 while(t--) 43 { 44 scanf("%d%d",&N,&M); 45 ans=0; 46 init(N); 47 while(M--) 48 { 49 getchar(); 50 scanf("%s%d%d",&buf,&x,&y);//用cin tle。。。。 51 op=buf[0]; 52 if(op=='D') 53 { 54 unio(x,y,1);//1代表着x,y不在同一类,即食物链中的x吃y。 55 } 56 else 57 { 58 int fx=find(x),fy=find(y); 59 if(fx==fy) 60 { 61 if((relat[x] - relat[y] +2)%2 ==1)//用食物链的观点来看,即x,y不在同一类。 62 { 63 printf("In different gangs.\n"); 64 } 65 else 66 printf("In the same gang.\n"); 67 } 68 else 69 { 70 printf("Not sure yet.\n"); 71 } 72 } 73 } 74 } 75 return 0; 76 }
POJ-1733 Parity game
题目大意:这题的大意是对于一个正整数的区间,有若干句话,判断第一句错误的位置,每句话所描述的意思是对于一个区间[a, b]有奇数个1或是偶数个1。
思路:
设s[i]表示前i个数中1的个数,则s[0]=0;则信息i j even等价于s[j]-s[i-1]为偶数,即s[j]与s[i-1]同奇偶。这样,每条信息都可以变为s[i-1]和s[j]是否同奇偶的信息。
若记:
fa[j]为当前和fa[j]同奇偶的元素集合,
diff[j]为和fa[j]不同奇偶的元素集合,
则一条信息i j even表明i-1,j有相同的奇偶性,将导致fa[j]和fa[i-1]合并,diff[j]和diff[i-1]合并;
i j odd表明i-1,j的奇偶性不同,将导致fa[j]和diff[i-1]合并(fa[j]与fa[i-1]奇偶性不同即与diff[i-1]奇偶性相同);diff[j]和fa[i-1]合并。
最后这题还必须得离散化,因为原来的区间太大,可以直接HASH一下,离散化并不会影响最终的结果,
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 #define MAXN 10010 6 #define HASH 9941 7 int N,K; 8 int fa[MAXN],diff[MAXN],rank[MAXN]; 9 int hash[MAXN]; 10 void init() 11 { 12 for(int i=0;i<MAXN;i++) 13 { 14 hash[i]=-1; 15 diff[i]=-1; 16 fa[i]=i; 17 rank[i]=0; 18 } 19 } 20 int find(int x) 21 { 22 if(x==-1) return -1; 23 if(x == fa[x]) return x; 24 fa[x]=find(fa[x]); 25 return fa[x]; 26 } 27 void unio(int x,int y) 28 { 29 if(x==-1 || y==-1) return; 30 int fx=find(x),fy=find(y); 31 if(fx==fy) return ; 32 if(rank[fx]>rank[fy]) 33 fa[fy]=fx; 34 else 35 { 36 fa[fx]=fy; 37 if(rank[fx]==rank[fy]) 38 rank[fy]++; 39 } 40 } 41 int main() 42 { 43 scanf("%d%d",&N,&K); 44 init(); 45 int a,b,sa,sb,da,db,ha,hb; 46 char s[10]; 47 for(int i=1;i<=K;i++) 48 { 49 scanf("%d%d%s",&a,&b,&s); 50 a--; 51 ha=a%HASH; 52 while(hash[ha] != -1 && hash[ha] !=a) 53 ha = (ha+1) %HASH; 54 hash[ha] = a; 55 a=ha; 56 hb = b % HASH; 57 while(hash[hb] != -1 && hash[hb] != b) 58 hb =(hb+1) %HASH; 59 hash[hb] =b; 60 b=hb; 61 //将a,b,diff[a],diff[b]的根结点找出来,再按要求合并 62 sa=find(a); 63 da=find(diff[a]); 64 sb=find(b); 65 db=find(diff[b]); 66 if(s[0]=='e') 67 { 68 if(sa== db || da==sb) 69 { 70 printf("%d\n",i-1); 71 return 0; 72 } 73 if(diff[a]==-1) diff[a]=db; 74 if(diff[b]==-1) diff[b]=da; 75 unio(sa,sb); 76 unio(da,db); 77 } 78 else if(s[0]=='o') 79 { 80 if(sa ==sb || (da != -1 && da== db)) 81 { 82 printf("%d\n",i-1); 83 return 0; 84 } 85 if(diff[a] == -1) diff[a] = sb; 86 if(diff[b] == -1) diff[b] = sa; 87 unio(sa, db); 88 unio(da, sb); 89 } 90 } 91 printf("%d\n",K); 92 return 0; 93 }
POJ-1988 Cube Stacking
题意:是给出N个立方体,可以将立方体移动到其它立方体形成堆,然后有P个下面的操作: 1) M X Y ,将X立方体所在的堆移到Y立方体所在的堆的上面; 2) C X 输出在X所在的堆上,在X立方体下面的立方体个数。
思路:
用三个数组,fa,ans,sum, fa[i]表示i的根结点,ans[i]表示i的结果,即压在i下面的立方体个数,sum[i]表示i所在的堆的立方体总个数。对于每一堆立方体,根结点使用堆底的立方体,而且在这个堆所对应的集合内,通过更新,使得只有根结点的sum值为这堆的总个数,ans值为0(因为它在堆底),其它的立方体的sum值都为0,ans值在并查集的查找步骤中进行递归更新。
在并查集的查找函数的执行中,先向上找到根结点,并且保存当前结点x的父节点为tmp,找到根结点后,向下依次一更新结点的ans,sum值。
1)若sum[x]不为0,即表示x是一个堆的堆底元素,ans[x]为0,其父节点是另外一堆的堆底(因为在并查集的操作中,通过将一个堆的堆底指向另一个堆的堆底来实现合并), ans[x]+=sum[tmp],sum[tmp]+=sum[x],sum[x]=0 ,这三个语句将x的ans值加上父结点的总个数(因为是将x所在的堆放在父节点的堆,所有x下面的正方体个数加上刚刚放上去的父亲的值),然后将父节点的sum值加上x的sum值(父节点的堆的总数变为两者之和),然后再将x的sum值置0.
2)若sum[x]为0,即表示x不是堆底,那么只要将x的ans值加上父节点(此父亲是原来的父亲tmp,因为在前面的更新中此父亲已经被更新了。所有他的ans值即为压在他下面的正方体个数)的ans值即可。ans[x]+=ans[tmp]。下面是并查集的几个函数。在合并操作里面,合并完后我们再对x,y执行一次查找操作以更新对应堆的值,因为在下次合并的时候可能堆还没有来得及更新。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 #define MAXN 30010 6 int fa[MAXN],ans[MAXN],sum[MAXN]; 7 int P; 8 void init() 9 { 10 for(int i=0;i<MAXN;i++) 11 { 12 fa[i]=i; 13 ans[i]=0; 14 sum[i]=1; 15 } 16 } 17 int find(int x) 18 { 19 int tmp; 20 if(x != fa[x]) 21 { 22 tmp=fa[x]; 23 fa[x]=find(fa[x]); 24 if(sum[x] != 0) 25 { 26 ans[x] += sum[tmp]; 27 sum[tmp] += sum[x]; 28 sum[x] =0; 29 } 30 else 31 { 32 ans[x] += ans[tmp]; 33 } 34 } 35 return fa[x]; 36 } 37 void unio(int x,int y) 38 { 39 int fx=find(x); 40 int fy=find(y); 41 fa[fx]=fy; 42 } 43 int main() 44 { 45 char c; 46 int a,b; 47 init(); 48 scanf("%d",&P); 49 while(P--) 50 { 51 getchar(); 52 scanf("%c",&c); 53 if(c=='M') 54 { 55 scanf("%d%d",&a,&b); 56 unio(a,b); 57 find(a);//每次合并后都得更新,防止下次合并出错 58 find(b); 59 } 60 else 61 { 62 scanf("%d",&a); 63 find(a); 64 printf("%d\n",ans[a]); 65 } 66 } 67 }
一点心得:
个人感觉对于那些种类并查集应该都可以用食物链的关系来理解的,通过记录与根结点的关系来判断是否在一个集合。。。刚刚把poj1703翻译成食物链版本,下次试试把上面这些题都翻译成食物链版本。。。。
一些其他的并查集题目汇总:
http://hi.baidu.com/czyuan%5Facm/blog/item/531c07afdc7d6fc57cd92ab1.html