並查集的應用

黎辰發表於2015-04-05

並查集的初級應用及進階

一、精華

精華提煉1:

  內容:並查集就是樹的孩子表示法的應用。

  解釋:對於下圖所示樹,它的孩子表示法為:

       

                   

             belg[5]=2, belg[6]=2, belg[7]=2;

             belg[2]=1, belg[3]=1, belg[4]=1;

             belg[1]=1(也可以=-1,只要能夠識別它是根就可以)

精華提煉2:

   內容:並查集的孩子父親表示法中,每個節點與其父親節點可以新增一個關係屬性(必須具有可傳遞性)。

   解釋:比如,節點表示一個人,關係屬性為一個人的性別。我們先用上圖來解釋這個關係屬性的應用,在後文具體展開。我們可以這樣定義,如果節點i和其父節點j性別相同(belg[i]=j),則kind[i]=false, 反之,kind[i]=true,那麼如果我們知道kind[5]=true,kind[2]=false,那麼5和2的父節點1的關係為kind[5]^kind[2]=true,即他們性別不同。 

 

二、基礎

基礎1:集合表示

根據精華提煉1,我們把一顆樹的節點集合看成以根節點命名的集合,那麼上面的集合我們可以認為是集合1。

下圖共有兩個集合,分別為集合1,集合2。

 

            

基礎2:元素關係

    如何判斷元素關係呢?其實,我們只需找出元素對應的集合名稱,然後判斷名稱是否相同即可。尋找集合名稱程式碼如下:

int Find(int x)

{

while ( belg[x]!=x )

{

    x = belg[x];

}

 

return x;

}

例如:對於基礎1中左圖,有belg[5]=2,belg[2]=2。那麼5屬於集合2。

    現在我們已經解決了元素關係問題。

基礎3:集合合併

集合如何合併呢?基礎2中,我們已經可以找到元素對應集合的名稱(即根節點標號),如果元素u、v(u、v不在同一集合)對應的集合名稱為_u、_v,那麼語句belg[_u]=_v什麼意思呢?想到了吧?就是把集合_u與集合_v合併,並且以_v命名。

 

至此,通過基礎部分我們知道了什麼是並查集,通過精華提煉部分,我們知道了並查集的高階應用(精華提煉2)。

 

三、優化

雖然我們已經知道了基礎的並查集,但是大家有沒有想過簡單用上面介紹的集合合併可能造成集合(樹)的退化。比如對只有一個元素的集合1到集合n進行下述操作:把集合1合併到集合2,把集合2合併到集合3,…… 把集合n-1合併到集合n,那麼生成一個含有n各元素的集合n,它的結構如下:

 

 

那麼,每次判斷n所屬集合都要n次操作,即複雜度為O(n),這個耗費是不是必須的呢?其實不然。

優化1:路徑壓縮

對於上圖退化的集合,它的表示是這樣的:belg[n]=n-1, belg[n-1]=n-2, …… belg[2]=1, belg[1]=1;

既然上面元素都屬於集合1,那麼我們是不是可以這樣做呢?belg[n]=1,belg[n-1]=1,……belg[2]=1,belg[1]=1;即把查詢n所屬集合時形成的路徑上的點直接連到根節點上。可以的,因為這樣操作只改變集合樹的結構,並沒有改變這個集合的元素。

關於路徑壓縮,可以在查詢過程中實現,那麼對於上述退化樹,查詢n第一次要n次操作,以後就只需一次操作。實現如下:

版本一:(遞迴)

int Find(int x)

{

  return x==belg[x]?x:(belg[x]=Find(belg[x]));

}

程式碼很短,遞迴次數多時,不建議使用。

版本二:(迭代)

int Find(int x)

{

  int _b, _x = x;

 

  while ( belg[_x]!=_x )

  {

     _x = belg[_x];     

  }

 

  while ( belg[x]!=x )

  {

     _b = belg[x];

     belg[x] = _x;

     x = _b;

  }

 

  return _x;

       }

       程式碼長點,但是少了遞迴過程,效率高點。

優化2:優化合並

     合理的安排合併方式,可以防止退化,例如對於上述退化的例子,我們把元素少的集合合併到元素多的集合上。即集合2合併到集合1,集合3合併到集合1,……集合n合併到集合1,那麼產生的樹結構為:

 

 

不過這個優化代價也很大的,因為要對開一個整型陣列來記錄集合元素個數,然後,再集合i和集合j合併時,通過判斷集合中元素個數來實現合併:

int Union(int i,int j)

{

   if ( sum[i]>sum[j] )

   {

     belg[j] = i;

   }

   else

   {

     belg[i] = j;

   }

}

細心的讀者,可能想到這個優化並不能完全避免集合退化,是的,所以我認為不必開闢陣列浪費空間進行這個優化,完全可以隨機法來由優化,比如:

int Union(int i,int j)

{

   if ( rand()&1 )

   {

     belg[j] = i;

   }

   else

   {

     belg[i] = j;

   }

}

通過隨機值的奇偶性來決定怎麼合併,平均效果是很好的。

   

上面詳細講了這麼多理論性的東西,下面開始介紹應用:

四、應用

基礎應用:

題目:

    有n個人(1..n),如果i和j是親戚,j和k是親戚,那麼j和k也是親戚,題目給定n各人的m對親戚關係,然後提出q各問題,問你某兩個人是不是親戚。

解答:

    並查集簡單應用,程式碼如下:

#include<iostream>

usingnamespace std;

 

constint MAXN = 1010;

 

int belg[MAXN];

 

int main()

{

int i, u, v, n, m, q;

 

scanf("%d", &n);

for ( i=1; i<=n; belg[i]=i,++i );

 

scanf("%d", &m);

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

{

    scanf("%d%d", &u, &v);

    u = Find(u); v = Find(v);

    if ( u!=v ) { Union(u,v); }

}

 

scanf("%d", &q);

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

{

    scanf("%d%d", &u, &v);

    u = Find(u); v = Find(v);

    printf("%s/n", (u==v?"YES":"NO"));

}

 

return 0; 

}

其中Find函式和Union函式參見上面的介紹。

 

高階應用:

題目:(HDU1829

有n各小動物,它們只有異性之間才配對,同性之間不會配對。給定m對配對關係,問你是否能通過分配性別給n各小動物,使這m各配對關係成立,即不會出現同性之間配對。

解答:

這裡我們使用在精華提煉二中提到的思路。

    首先,我們必須明確兩點:1.這裡的屬於同一個集合的元素表示他們的關係已經確定,比如元素i和元素j屬於同一個集合,那麼他們要麼同性,要麼異性,關係時確定的。2.同一個集合的樹表示中,節點i和它的父親節點j關係儲存在kind[i]中。

同時,我們約定,如果節點i和節點j性別相同,則關係為false,否則關係為true。根節點root滿足kind[root]=false,因為自己跟自己性別肯定相同(當然不包括人妖了哈^-^)。

關係的運算我們可以通過異或(提示1)來實現,如果i和j關係為r1,i和k關係為r2,那麼j和k關係為r1^r2。

上面的分析已經足夠我們處理這個題目了。下面給出程式碼:

#include<iostream>

usingnamespace std;

 

constint MAXN = 2010;

 

int  belg[MAXN];

bool kind[MAXN];

 

int Find(int x,bool &s);

 

int main()

{

    int  i, k, n, m;

    int  u, v, _u, _v, cas;

    bool flag, su, sv;

 

    scanf("%d", &cas);

 

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

    {

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

 

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

        {

            belg[i] = i;

            kind[i] =false;

        }

 

        for ( i=1,flag=true; i<=m; ++i )

        {

            scanf("%d%d", &u, &v);

 

            if ( flag )

            {

                _u = Find(u,su=false);

                _v = Find(v,sv=false);

 

                if ( _u==_v )

                {

                    flag = su^sv;

                }

                else

                {

                    belg[_u] = _v;

                    kind[_u] = !(su^sv);

                }

            }

        }

 

        printf("Scenario #%d:/n", k);

        if ( flag )

        {

            printf("No suspicious bugs found!/n/n");

        }

        else

        {

            printf("Suspicious bugs found!/n/n");

        }

    }

 

    return 0;

}

 

int Find(int x,bool &s)

{

    int h;

 

    if ( belg[x]==x )

    {

        h = x; s = false;   

    }

    else

    {

        h = Find(belg[x],s);

        belg[x] = h;

        s = kind[x]^s;

        kind[x] = s;

    }

 

    return h;

}

    由於上述Find函式使用了遞迴所以比較耗時(1609毫秒,132KB),可以改為如下的迭代形式(671毫秒,0KB):

int Find(int x,bool &s)

{

    int _x, h = x;

    bool s1, s2;

 

    while ( belg[h]!=h )

    {

        s = s^kind[h];

        h = belg[h];       

    }

 

    s1 = s;

    while ( belg[x]!=x )

    {

        _x = belg[x];

        belg[x] = h;

        s2 = kind[x];

        kind[x] = s1;

        s1 = s1^s2;

        x = _x;

    }

 

    return h;

}

 

提示1.異或:i和j異或就是:如果i和j相同則為false,否則為true,比如i=true,j=false,則i異或j為true。i=false,j=false,則i異或j為false。

相關文章