並查集 (Union-Find Sets)及其應用

fisher_jiang發表於2006-08-25

並查集 (Union-Find Sets)

並查集:(union-find sets)是一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多。一般採取樹形結構來儲存並查集,並利用一個rank陣列來儲存集合的深度下界,在查詢操作時進行路徑壓縮使後續的查詢操作加速。這樣優化實現的並查集,空間複雜度為O(N),建立一個集合的時間複雜度為O(1)N次合併M查詢的時間複雜度為O(M Alpha(N)),這裡AlphaAckerman函式的某個反函式,在很大的範圍內(人類目前觀測到的宇宙範圍估算有1080次方個原子,這小於前面所說的範圍)這個函式的值可以看成是不大於4的,所以並查集的操作可以看作是線性的。它支援以下三中種操作:
  -Union (Root1, Root2) //並操作;把子集合Root2併入集合Root1.要求:Root1 Root2互不相交,否則不執行操作
.
  -Find (x) //搜尋操作;搜尋單元素x所在的集合,並返回該集合的名字
.
  -UFSets (s) //建構函式。將並查集中s個元素初始化為s個只有一個單元素的子集合
.
  -對於並查集來說,每個集合用一棵樹表示。

  -集合中每個元素的元素名分別存放在樹的結點中,此外,樹的每一個結點還有一個指向其雙親結點的指標。
  -設 S1= {0, 6, 7, 8 }S2= { 1, 4, 9 }S3= { 2, 3, 5 }

-為簡化討論,忽略實際的集合名,僅用表示集合的樹的根來標識集合。
  -為此,採用樹的雙親表示作為集合儲存表示。集合元素的編號從0 n-1。其中 n 是最大元素個數。在雙親表示中,第 i 個陣列元素代表包含集合元素 i 的樹結點。根結點的雙親為-1,表示集合中的元素個數。為了區別雙親指標資訊( ≥ 0 ),集合元素個數資訊用負數表示。
   

下標
parent

集合S1, S2S3的雙親表示:

                             S1 S2的可能的表示方法

const int DefaultSize = 10;
  class UFSets { //並查集的類定義

  private:
   
int *parent;
   
int size;
  
public:
   
UFSets ( int s = DefaultSize );
   
~UFSets ( ) { delete [ ] parent; }
   UFSets & operator = ( UFSets const & Value );//集合賦值

   void Union ( int Root1, int Root2 );
   
int Find ( int x );
   
void UnionByHeight ( int Root1, int Root2 ); };
   UFSets::UFSets ( int s ) { //建構函式

   size = s;
   
parent = new int [size+1];
   
for ( int i = 0; i <= size; i++ ) parent[i] = -1;
  }

  unsigned int UFSets::Find ( int x ) { //搜尋操作
   if ( parent[x] <= 0 ) return x;
   
else return Find ( parent[x] );
  }

  void UFSets::Union ( int Root1, int Root2 ) { //
   parent[Root2] = Root1; //Root2指向Root1
  }

FindUnion操作效能不好。假設最初 n 個元素構成 n 棵樹組成的森林,parent[i] = -1。做處理Union(0, 1), Union(1, 2), …, Union(n-2, n-1)後,將產生如圖所示的退化的樹。

                            

執行一次Union操作所需時間是O(1)n-1Union操作所需時間是O(n)。若再執行Find(0), Find(1), …, Find(n-1), 若被
搜尋的元素為i,完成Find(i)操作需要時間為O(i),完成 n 次搜尋需要的總時間將達到
              

Union操作的加權規則

  為避免產生退化的樹,改進方法是先判斷兩集合中元素的個數,如果以 i 為根的樹中的結點個數少於以 j 為根的樹中的結點個數,即parent[i] > parent[j],則讓 j 成為 i 的雙親,否則,讓i成為j的雙親。此即Union的加權規則。

              parent[0](== -4) < parent[4] (== -3)

 

  void UFSets::WeightedUnion(int Root1, int Root2) {
   //Union的加權規則改進的演算法

   int temp = parent[Root1] + parent[Root2];
   
if ( parent[Root2] < parent[Root1] ) {
    parent[Root1] = Root2; //Root2中結點數多

    parent[Root2] = temp;  //Root1指向Root2
   }
   
else {
    parent[Root2] = Root1; //Root1中結點數多

    parent[Root1] = temp;  //Root2指向Root1
   }
  }

                            使用加權規則得到的樹

 

下面是幾到用並查集可以方便解決的問題:

 

題目: 親戚(Relations)

或許你並不知道,你的某個朋友是你的親戚。他可能是你的曾祖父的外公的女婿的外甥的表姐的孫子。如果能得到完整的家譜,判斷兩個人是否親戚應該是可行的,但如果兩個人的最近公共祖先與他們相隔好幾代,使得家譜十分龐大,那麼檢驗親戚關係實非人力所能及.在這種情況下,最好的幫手就是計算機。

為了將問題簡化,你將得到一些親戚關係的資訊,如同MarryTom是親戚,TomB en是親戚,等等。從這些資訊中,你可以推出MarryBen是親戚。請寫一個程式,對於我們的關心的親戚關係的提問,以最快的速度給出答案。

參考輸入輸出格式 輸入由兩部分組成。

第一部分以NM開始。N為問題涉及的人的個數(1 N 20000)。這些人的編號為1,2,3,,N。下面有M(1 M 1000000),每行有兩個數ai, bi,表示已知aibi是親戚.

第二部分以Q開始。以下Q行有Q個詢問(1 Q 1 000 000),每行為ci, di,表示詢問cidi是否為親戚。

對於每個詢問ci, di,若cidi為親戚,則輸出Yes,否則輸出No

樣例輸入與輸出

輸入relation.in

10 7

2 4

5 7

1 3

8 9

1 2

5 6

2 3

3

3 4

7 10

8 9

輸出relation.out

Yes

No

Yes

如果這道題目不用並查集,而只用連結串列或陣列來儲存集合,那麼效率很低,肯定超時。

例程:

 

#include<iostream>

using namespace std;

int N,M,Q;

int pre[20000],rank[20000];

void makeset(int x)

 {

     pre[x]=-1;

     rank[x]=0;

 }

int find(int x)

 {

     int r=x;

     while(pre[r]!=-1)

      r=pre[r];

     while(x!=r)

      {

          int q=pre[x];

          pre[x]=r;

          x=q;

      }

    return r;      

 }    

void unionone(int a,int b)

 {

     int t1=find(a);

     int t2=find(b);

     if(rank[t1]>rank[t2])

       pre[t2]=t1;

    else

       pre[t1]=t2;

    if(rank[t1]==rank[t2])

      rank[t2]++;     

 }       

int main()

 

{

   int i,a,b,c,d;

    while(cin>>N>>M)

     {

         for(i=1;i<=N;i++)

          makeset(i);

        for(i=1;i<=M;i++)

          {

              cin>>a>>b;

              if(find(a)!=find(b))

               unionone(a,b);

          }

        cin>>Q; 

        for(i=1;i<=Q;i++)

         {

             cin>>c>>d;

             if(find(c)==find(d))

              cout<<"YES"<<endl;

             else

              cout<<"NO"<<endl; 

         }           

     }   

    return 0;

}

 

ZJU1789The Suspects

【問題描述】

 

Severe acute respiratory syndrome (SARS), an atypical pneumonia of unknown aetiology, was recognized as a global threat in mid-March 2003. To minimize transmission to others, the best strategy is to separate the suspects from others.

In the Not-Spreading-Your-Sickness University (NSYSU), there are many student groups. Students in the same group intercommunicate with each other frequently, and a student may join several groups. To prevent the possible transmissions of SARS, the NSYSU collects the member lists of all student groups, and makes the following rule in their standard operation procedure (SOP).

Once a member in a group is a suspect, all members in the group are suspects.

However, they find that it is not easy to identify all the suspects when a student is recognized as a suspect. Your job is to write a program which finds all the suspects.


Input

The input contains several cases. Each test case begins with two integers n and m in a line, where n is the number of students, and m is the number of groups. You may assume that 0 < n <= 30000 and 0 <= m <= 500. Every student is numbered by a unique integer between 0 and n-1, and initially student 0 is recognized as a suspect in all the cases. This line is followed by m member lists of the groups, one line per group. Each line begins with an integer k by itself representing the number of members in the group. Following the number of members, there are k integers representing the students in this group. All the integers in a line are separated by at least one space.

A case with n = 0 and m = 0 indicates the end of the input, and need not be processed.


Output

For each case, output the number of suspects in one line.


Sample Input

100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0


Sample Output

4
1
1

【演算法分析】

 

這道題的意思很簡單,n個人編號,0n-1,n個人分成m個集合(1個人可以參加不同的集合),求的就是最後所有和0號有關係的集合的

人數.

如果這道題目不用並查集,而只用連結串列或陣列來儲存集合,那麼效率很低,肯定超時.我們在題目給出的每個集合的人員編號時,進行並查

操作,不過在進行合併操作時,合併的是兩個集合的元素個數.最後0號元素所在的集合數目就是所求.

例程:

#include<iostream>

#include<cstdio>

using namespace std;

const int size=30000;

int pre[size],num[size];

int n,m,k;

void makeset(int x)

 {

     pre[x]=-1;

     num[x]=1;

 }

int find(int x)//非遞迴壓縮路徑

 {

     int r=x;

     while(pre[r]!=-1)

      r=pre[r];

     while(x!=r)

      {

          int q=pre[x];

          pre[x]=r;

          x=q;

      }

    return r;      

 }    

int unionone(int a,int b)

{

    int t1,t2;

    t1=find(a);

    t2=find(b);

    if(t1==t2) return 0;

    if(num[t2]<=num[t1])

    {

        pre[t2]=t1;

        num[t1]+=num[t2];

    }

    else

    {

        pre[t1]=t2;

        num[t2]+=num[t1];

    }

    return 0;

}

int main()

 {  

     freopen("in.txt","r",stdin);

     freopen("out.txt","w",stdout);

     int i,j,a,b;

     while(scanf("%d%d",&n,&m))

      {

          if(n==0&&m==0)

           break;

          for(i=0;i<n;i++)

            makeset(i);

          for(i=0;i<m;i++)

           {

               //cin>>k;

               scanf("%d",&k);

               if(k==0) continue;

               //cin>>a;

                scanf("%d",&a);

                a=find(a);

               for(j=1;j<k;j++)

                {

                    //cin>>b;

                    scanf("%d",&b);

                    b=find(b);

                    unionone(a,b);

                }   

           }

         printf("%d/n",num[find(0)]);      

      }   

     return 0;

 }    

 

  

   

    

     

       

 

 

銀河英雄傳說

 

【問題描述】

        公元五八○一年,地球居民遷移至金牛座α第二行星,在那裡發表銀河聯邦創立宣言,同年改元為宇宙曆元年,並開始向銀河系深處擴充。

        宇宙歷七九九年,銀河系的兩大軍事集團在巴米利恩星域爆發戰爭。泰山壓頂集團派宇宙艦隊司令萊因哈特率領十萬餘艘戰艦出征,氣吞山河集團點名將楊威利組織麾下三萬艘戰艦迎敵。

        楊威利擅長排兵佈陣,巧妙運用各種戰術屢次以少勝多,難免恣生驕氣。在這次決戰中,他將巴米利恩星域戰場劃分成30000列,每列依次編號為1, 2, …, 30000。之後,他把自己的戰艦也依次編號為1, 2, …, 30000,讓第i號戰艦處於第i(i = 1, 2, …, 30000),形成“一字長蛇陣”,誘敵深入。這是初始陣形。當進犯之敵到達時,楊威利會多次釋出合併指令,將大部分戰艦集中在某幾列上,實施密集攻擊。合併指令為M i j,含義為讓第i號戰艦所在的整個戰艦佇列,作為一個整體(頭在前尾在後)接至第j號戰艦所在的戰艦佇列的尾部。顯然戰艦佇列是由處於同一列的一個或多個戰艦組成的。合併指令的執行結果會使佇列增大。

        然而,老謀深算的萊因哈特早已在戰略上取得了主動。在交戰中,他可以通過龐大的情報網路隨時監聽楊威利的艦隊調動指令。

        在楊威利釋出指令調動艦隊的同時,萊因哈特為了及時瞭解當前楊威利的戰艦分佈情況,也會發出一些詢問指令:C i j。該指令意思是,詢問電腦,楊威利的第i號戰艦與第j號戰艦當前是否在同一列中,如果在同一列中,那麼它們之間佈置有多少戰艦。

        作為一個資深的高階程式設計員,你被要求編寫程式分析楊威利的指令,以及回答萊因哈特的詢問。

        最終的決戰已經展開,銀河的歷史又翻過了一頁……

 

【輸入檔案】

輸入檔案galaxy.in的第一行有一個整數T1<=T<=500,000),表示總共有T條指令。

以下有T行,每行有一條指令。指令有兩種格式:

1.        M  i  j  ij是兩個整數(1<=i , j<=30000),表示指令涉及的戰艦編號。該指令是萊因哈特竊聽到的楊威利釋出的艦隊調動指令,並且保證第i號戰艦與第j號戰艦不在同一列。

2.        C  i  j  ij是兩個整數(1<=i , j<=30000),表示指令涉及的戰艦編號。該指令是萊因哈特釋出的詢問指令。

 

【輸出檔案】

輸出檔案為galaxy.out。你的程式應當依次對輸入的每一條指令進行分析和處理:

如果是楊威利釋出的艦隊調動指令,則表示艦隊排列發生了變化,你的程式要注意到這一點,但是不要輸出任何資訊;

 如果是萊因哈特釋出的詢問指令,你的程式要輸出一行,僅包含一個整數,表示在同一列上,第i號戰艦與第j號戰艦之間佈置的戰艦數目。如果第i號戰艦與第j號戰艦當前不在同一列上,則輸出-1

 

【樣例輸入】

4

M 2 3

C 1 2

M 2 4

C 4 2

 

【樣例輸出】

-1

1

 

【樣例說明】

戰艦位置圖:表格中阿拉伯數字表示戰艦編號

 

第一列

第二列

第三列

第四列

……

初始時

1

2

3

4

……

M 2 3

1

 

3

2

4

……

C 1 2

1號戰艦與2號戰艦不在同一列,因此輸出-1

M 2 4

1

 

 

4

3

2

……

C 4 2

4號戰艦與2號戰艦之間僅佈置了一艘戰艦,編號為3,輸出1

 

【演算法分析】

        同一列的戰艦組成一個並查集,在集合中,我們以當前列的第一艘戰艦作為集合的代表元.並查集的資料型別採用樹型,樹的根結點即為集合的代表元.為了查詢的效率達到最優,我們進行了路徑壓縮的優化:首先找到樹根,然後將路徑上所有結點的父結點改為根,使得樹的深度為1.

        問題是,題目不僅要求判別兩個結點是否在同一個集合(即兩艘戰艦是否在同一列),而且還要求計算結點在有序集合的位置(即每一艘戰艦相隔列的第一艘戰艦幾個位置),       我們增加了一個陣列behind[x],記錄戰艦x在例中的相對位置.

        查詢一個元素x所在集合的代表元時,先從x沿著父親節點找到這個集合的代表元root,然後再從x開始一次到root的遍歷,累計其間經過的每一個子結點的behind,其和即為behind[i].,如下圖所示:

          

       按照題意,合併指令Mxy,含義是讓戰艦x 所在的整個戰艦佇列,作為一個整體(頭在前,尾在後)接至戰艦y所在的戰艦佇列的尾部,顯然兩個佇列合併成同一列後,其集合代表元為結點y所在的樹的根結點fy,x所在的樹的根結點fx,合併後,fx的相對位置為合併前y所在集合的結點數, behind[fx]=num[fy],新集合的結點數為原來兩個集合結點數的和 num[fy]+=num[fx]. 則如果戰艦x和戰艦y在同一列,則他們相隔

|behind[x]-behind[y]|-1艘戰艦.如下圖:

 

例程:

#include<iostream>

#include<cstdio>

#include<cmath>

using namespace std;

const int size=30001;

int pre[size],num[size],behind[size];//behind[x]戰艦x在列中的相對位置

int n,m,k;

void makeset(int x)

 {

     pre[x]=-1;

     num[x]=1;

 }

int find(int x)//查詢結點x所在樹的根結點,並對該樹進行路徑壓縮

 {

     int r=x;

     int j;

     while(pre[r]!=-1)//找出結點x所在樹的根結點r

      r=pre[r];

     while(x!=r)

      {

          int q=pre[x];//路徑壓縮

          pre[x]=r;

          j=q;

          do{//迭代求出路徑上每一個子結點相對於r的相對位置

              behind[x]+=behind[j];

              j=pre[j];

            }while (j!=-1);   

          x=q;

      }

    return r;      

 }    

void MoveShip(int a,int b)

{

    int t1,t2;

    t1=find(a);//計算a所在樹的根結點t1

    t2=find(b);//計算b所在樹的根結點t2

    pre[t1]=t2;//將t1的父結點設為t2

    behind[t1]=num[t2];//計算t1的相對位置為num[t2]

    num[t2]+=num[t1]; //計算新集合的結點數

 

}

 

void CheckShip(int x,int y)

 {

     int f1,f2;

     f1=find(x);

     f2=find(y);

     if(f1!=f2)

      cout<<-1<<endl;

     else

      cout<<abs(behind[x]-behind[y])-1<<endl;

 }

int main()

 {

   freopen("galaxy.in","r",stdin);

    freopen("out.txt","w",stdout);

 

     int n,x,y;

     char ch;

     while(cin>>n)

      {

          for(int i=1;i<size;i++)

           {

               makeset(i);

           }

          memset(behind,0,sizeof(behind));   

          while(n--)

           {

               cin>>ch>>x>>y;

               if(ch=='M')

                 MoveShip(x,y); //處理合並指令

               else if(ch=='C')

                 CheckShip(x,y);  //處理詢問指令

           }   

      } 

   return 0;    

 }       

 

 

 

相關文章