1. 程式人生 > >資料結構之並查集

資料結構之並查集

1.概述

英文:DisjointSet or(Union-find set),即“不相交集合”將編號分別為1…N的N個物件劃分為不相交集合,在每個集合中,選擇其中某個元素代表所在集合。

常見兩種操作:

(1)兩個集合      (2)找某元素屬於哪個集合 所以也成稱為並查集。。

2.導引問題

Problem Description 某省調查城鎮交通狀況,得到現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府“暢通工程”的目標是使全省任何兩個城鎮間都可以實現交通(但不一定有直接的道路相連,只要互相間接通過道路可達即可)。問最少還需要建設多少條道路?  Input 測試輸入包含若干測試用例。每個測試用例的第1行給出兩個正整數,分別是城鎮數目N ( < 1000 )和道路數目M;隨後的M行對應M條道路,每行給出一對正整數,分別是該條道路直接連通的兩個城鎮的編號。為簡單起見,城鎮從1到N編號。 
注意:兩個城市之間可以有多條道路相通,也就是說
3 3
1 2
1 2
2 1
這種輸入也是合法的
當N為0時,輸入結束,該用例不被處理。  Output 對每個測試用例,在1行裡輸出最少還需要建設的道路數目。 

如果遇到這題,我們該如何去做呢?

3.並查集引入

初始化操作:

我們需要一個 void makeset (int  n)n表示初始化的範圍

void makeset(int n)
{
     for(int i = 0; i <= n; i++)
        father[i] = i;
}

查詢操作:

假如我們現在有要在這個集合中尋找一個元素x所處的集合(該元素的符節點),那麼我們只需要一個 int findset(int a)函式

//遞迴寫法
int findset(int a){
     if(a==father[a])  return a;
     return findset(father[a]);
}
//迭代寫法
int findset(int x){
    int p = x;
    while(p!=father[p]) p = father[p];
    return p;
}
findset返回的值為該元素根節點的下標

合併操作:

假如 x 和 y開始是屬於不同集合的元素,現在要把它們併到一起,那麼需要一個 void unionset (int x , int y)函式

void unionset(int x, int y)
{
     int a = findset(x);
     int b = findset(y);
     if(a != b)  father[a] = b;
}

有了以上關於並查集的基礎,相信都可以寫HDU1232這道題了。。

#include <iostream>
#define SIZE 10005
using namespace std;

int father[SIZE];
void makeset(int n)
{
     for(int i = 0; i <= n; i++)
        father[i] = i;
}
int findset(int a){
     if(a == father[a])  
        return a;
     return findset(father[a]);
}
void unionset(int x, int y)
{
     int a = findset(x);
     int b = findset(y);
     if(a != b)  father[a] = b;
}
int main()
{
    int n, m;
    int x, y;
    int ans = 0;
    while(cin>>n>>m)
    {
       makeset(n);
       for(int i = 1; i <= m; i++)
       {
         cin>>x>>y;
         unionset(x,y);
       }
       for(int i = 1; i <= n; i++)
       {
         if(father[i] == i)
            ans++;
       }
       ans--;
       cout<<ans<<endl;
       ans = 0;
    }
}

4.並查集的優化

Find_Set(x)時 路徑壓縮
尋找祖先時我們一般採用遞迴查詢,但是當元素很多亦或是整棵樹變為一條鏈時,每次Find_Set(x)都是O(n)的複雜度,有沒有辦法減小這個複雜度呢?
答案是肯定的,這就是路徑壓縮,即當我們經過"遞推"找到祖先節點後,"回溯"的時候順便將它的子孫節點都直接指向祖先,這樣以後再次Find_Set(x)時複雜度就變成O(1)了,如下圖所示;可見,路徑壓縮方便了以後的查詢。

Union(x,y)時 按秩合併
即合併的時候將元素少的集合合併到元素多的集合中,這樣合併之後樹的高度會相對較小。

int findset (int a)

//遞迴寫法
int findset(int a){
     if(a==father[a])  
        return a;
     else {
       int temp = father[a];
       father[a] = findset(father[a]);   
     } 
     return father[a]; //father[a] 已經是根節點,直接返回就行了
}

//非遞迴的方式進行路徑壓縮,更加直觀一些
int findset(int x){
    int p = x,temp;
    while(p!=father[p]) p = father[p];
    //fatherpath comfatherression
    while(x != p){
        temp = father[x];
        father[x] = p;
        x = temp;
    }
    return p;
}
5.最終模板
void makeset(int n){
    int i;
    for(i=1;i<=n;i++){
       father[i] = i; 
       rank[i] = 1;                 
    }
}

int findset(int a){
     if(a==father[a])  
        return a;
     else {
       int temp = father[a];
       father[a] = findset(father[a]);   
       rank[a] = (rank[temp]+rank[a]+1)%2 ; 
    //必須有,更新路徑壓縮之後a與根結點之間的關係; father改變,rank就必須要跟著改變 
     } 
     return father[a];
}

void unionset(int a,int b){
     int fa,fb;
     fa = findset(a);
     fb = findset(b);
     if(fa!=fb){
         father[fa] = fb;   
         rank[fa] = (rank[a]+rank[b])%2 ;     //fa結點以下的結點的rank不需要改               
     }
}

6相關題目練習

待更新

參考目錄:

http://www.cnblogs.com/cherish_yimi/archive/2009/10/11/1580839.html