數據結構---並查集小結
By-Missa
並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合並及查詢問題。 (百度百科)
大體分為三個:普通的並查集,帶種類的並查集,擴展的並查集(主要是必須指定合並時的父子關系,或者統計一些數據,比如此集合內的元素數目。)
View Code
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
模版最小生成樹:
View Code
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。(如下圖:)

View Code
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]里面。
View Code
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的食物鏈改編版:
View Code
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一下,離散化並不會影響最終的結果,
View Code
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執行一次查找操作以更新對應堆的值,因為在下次合並的時候可能堆還沒有來得及更新。
View Code
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
