數據結構之並查集小結


數據結構---並查集小結

                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]x2表示xfa[x]relat[]可以抽象成元素i到它的父親節點的邏輯距離,見下面。}

    怎樣判斷一句話是不是假話?
       假設已讀入 D , X , Y , 先利用find()函數得到X , Y 所在集合的代表元素 fx,fy ,若它們在同一集合(即 fx== fy )則可以判斷這句話的真偽:
       1.若 D == 1 (XY同類)而 relat[X] != relat[Y] 則此話為假。D == 1 表示XY為同類,而從relat[X] != relat[Y]可以推出 與 不同類。比如relat[x]=0  fxx同類,relat[y]=1 fyy,fx==fy,故矛盾。)
       2.若 D == 2 XY)而 relat[X] == relat[Y] Y為同類,故矛盾。)或者 relat[X] == ( relat[Y] + 1 ) % 3 Y)則此話為假。

上個問題中 r[X] == ( r[Y] + 1 ) % 3這個式子怎樣推來?

我們來列舉一下:   假設有YX注意fx==fy的前提條件),那么r[X]r[Y]的值是怎樣的?
                            r[X] = 0 && r[Y] = 2 Xfx同類,Yfy,YX) 
                            r[X] = 1 && r[Y] = 0  (Xfx吃,Yfy同類,即YX)
                            r[X] = 2 && r[Y] = 1  (Xfx,Yfy吃,一個環,YX)
通過觀察得到r[X] = ( r[Y] + 1 ) % 3;

對於上個問題有更一般的判斷方法(來自poj 1182中的Discuss ):
   若 ( r[x] - r[y] + 3 ) % 3 != d - 1(d-1 值是1或者0.....) ,則此話為假。

當判斷兩個元素的關系時,若它們不在同一個集合當中,則它們還沒有任何關系,直接將它們按照給出的關系合並就可以了。若它們在同一個集合中,那么它們的關系就是xy的距離:如圖所示為(r[x]+3-r[y])%3,即xy的已有的關系表達,判斷和給出的關系是否一致就可以知道是真話還是假話了。 

 

注意事項:

A、find()函數里面的那句relat[x]=(relat[x] + relat[t])%3解釋:

我們x--r-->y表示xy之間的關系是r,比如x--1--y代表xy。現在,若已知x--r1-->yy--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;xroot的關系是xroot吃。。。。其他類似。

用邏輯距離理解如下(向量思想):

 

 

B、當D X Y時,則應合並X的根節點和Y的根節點,同時修改各自的relat。那么問題來了,合並了之后,被合並的根節點的relat值如何變化呢?
現有xydxy的關系,fxfy分別是xy的根節點,於是我們有x--relat[x]-->fxy--relat[y]-->fy,顯然我們可以得到fx--(3-relat[x])-->xfy--(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。我們求解了fxfy的關系。即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]命令的話,即bc不在同一個幫派,故bdiff[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. 根元素不同,但bdiff[c]的根元素相同,b,c不在一個集合里;

3.否則,bc還沒有確定。

POJ-2492與1703基本一樣。

另解(更一般的解)這兩題可以用食物鏈的形式寫,即簡化的食物鏈,比食物鏈少一個關系,即相當於1221

對應關系即為:

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-1j有相同的奇偶性,將導致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值都為0ans值在並查集的查找步驟中進行遞歸更新。   
  在並查集的查找函數的執行中,先向上找到根結點,並且保存當前結點x的父節點為tmp,找到根結點后,向下依次一更新結點的ans,sum值。
      1)sum[x]不為0,即表示x是一個堆的堆底元素,ans[x]0,其父節點是另外一堆的堆底(因為在並查集的操作中,通過將一個堆的堆底指向另一個堆的堆底來實現合並), ans[x]+=sum[tmp],sum[tmp]+=sum[x],sum[x]=0 ,這三個語句將xans值加上父結點的總個數(因為是將x所在的堆放在父節點的堆,所有x下面的正方體個數加上剛剛放上去的父親的值),然后將父節點的sum值加上xsum(父節點的堆的總數變為兩者之和),然后再將xsum值置0.
      2)sum[x]0,即表示x不是堆底,那么只要將xans值加上父節點(此父親是原來的父親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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM