並查集—應用

逐夢起航-帶夢飛翔發表於2017-08-14

有人說:並查集好寫,好用,就是沒什麼地方用。的確,並查集應用不是特別廣,但作為一種優質演算法,這裡還是要多說幾句。並查集的問題主要分成兩大類:帶權並查集,種類並查集。


一、帶權並查集

帶權並查集就是讓每個節點除了記錄自己父親以外,還記錄一些其它的東西(如:集合的大小),通過它記錄的資訊來解決題目。


例題1:(來源: caioj 1095)

1. M i j :合併指令,i和 j是指令涉及的戰艦編號。該指令是將i號戰艦所在的整個戰艦接至第 j號戰艦所在的戰艦佇列的尾部。
2. C i j :詢問指令,i和 j是指令涉及的戰艦編號。第 i 號戰艦與第 j 號戰艦當前是否在同一列中,如果在同一列中,那麼它們之間佈置有多少戰艦。 


看到“合併”,很容易想到並查集,這題合併的戰艦要求保持有序,那對並查集的要求有點高了。

思路:我們可以對戰艦組進行編號,戰艦頭記為1,戰艦尾在艦隊中的編號特別記錄在tail中,由戰艦頭負責管理。在戰艦組中的每艘戰艦要記住該戰艦組的頭,它就像是艦隊的首領,同時,不能忘記自己在戰艦組中的位置。這樣,對於每次詢問,我們只要先看看這兩艘戰艦是否在同一戰艦組,是,則輸出它們的位置差。


程式碼:

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
 
int fa[30010],dis[30010],tail[30010];//dis記錄戰艦在艦隊中的位置  tail記錄該戰艦的尾戰艦在艦隊中的編號,也能反映艦隊的大小 
char c[5];
 
int find_fa(int x)
{
    if(fa[x]==x) return fa[x];
    int f=find_fa(fa[x]);//先讓艦隊頭更新當前時局 
    dis[x]=dis[x]+dis[fa[x]]-1;//自己再更新 
    fa[x]=f;
    return fa[x];
}
 
int main()
{
    int T;
    scanf("%d",&T);
    for(int i=1;i<=30000;i++)
    {
        fa[i]=i;
        dis[i]=1;
        tail[i]=1;
    }
    while(T--)
    {
        int x,y;
        scanf("%s%d%d",c,&x,&y);
        if(c[0]=='M')
        {
            int fx=find_fa(x),fy=find_fa(y);
            fa[fx]=fy;
            dis[fx]=tail[fy]+1;//fx更新自己在艦隊中的位置 
            tail[fy]=tail[fy]+tail[fx];//fy更新擴充套件後的艦隊的資訊
        }
        else
        {
            int fx=find_fa(x),fy=find_fa(y);
            if(fx!=fy)
            {
                printf("-1\n");
                continue;
            }
            printf("%d\n",abs(dis[x]-dis[y])-1);
        }
    }
    return 0;
}


二、種類並查集

種類並查集就是已經給出了兩兩關係判斷方式的並查集,要求你根據這種判斷方式來求出其中某兩個的關係。通常我們需要用一個re陣列,記錄下我與我父親的關係,此外還要求出關係的轉移式,使得每個集合內的關係和合並集合時的關係能夠準確無誤的展現。


例題2:(來源: caioj 1096)

動物王國中有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B, B吃C,C吃A。
第一種說法是“1 X Y”,表示X和Y是同類。 
第二種說法是“2 X Y”,表示X吃Y。
當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。當前的話與前面的某些真的話衝突,就是假話;當前的話中X或Y比N大,就是假話;當前的話表示X吃X,就是假話。 輸出假話的總數。


如果用一個belong陣列簡單記錄下每種動物的屬性,在合併兩條食物鏈時,會顯得無從下手,既然又栽在了合併的問題上,為什麼不考慮並查集呢?

思路:用re陣列記錄下我與父親的關係:0:我與父親同類,或者我沒有父親;1:我吃父親;2:我被父親吃。


如圖(箭頭從吃指向被吃),我們來理一理關係的計演算法則。

D與A的關係應該為0,其實是(re[x]+re[fa[x]])%3的結果。由此,我們得到,一條食物鏈上的關係可以用加法解決。

A與C的關係應該為1(即A吃C),其實是(3-re[x])%3的結果。由此,我們得到,若將關係的發出者交換,關係為(3-re[x])%3

B與C的關係應該為2(即B被C吃),根據結論2,我們可以把B與A的關係和C與A的關係,轉換為B與A的關係和A與C的關係,現在B和C就在一條順著的關係上了。再根據結論1,我們得到B與C的關係為(3-re[y])%3+re[x],整理得(re[x]-re[y]+3)%3。由此,我們求得了與祖先有直接關係的動物跨過祖先後,它們關係為(re[x]-re[y]+3)%3


接下來,如果發現X與Y在一條食物鏈裡,我們讓X與Y直接與父親相連(路徑壓縮),那麼我們通過 (re[x]-re[y]+3)%3 可以找到X與Y的關係;特別的,當X或Y沒有父親時,也不影響通過該公式求出兩個動物間的關係。


程式碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
 
int n,k,ans=0;
int fa[50010],re[50010];//x與fa[x]的關係(0,1,2) 
 
int find_fa(int x)
{
    if(fa[x]==x) return fa[x];
    int f=find_fa(fa[x]);
    re[x]=(re[x]+re[fa[x]])%3;//關係公式 
    fa[x]=f;
    return fa[x];
}
 
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
        re[i]=0;
    }
    while(k--)
    {
        int d,x,y;
        scanf("%d%d%d",&d,&x,&y);
        if(x>n||y>n) ans++;
        else if(d==2&&x==y) ans++;
        else
        {
            int fx=find_fa(x),fy=find_fa(y);
            if(fx==fy)//在一條記錄好了的食物鏈中 
            {
                if((re[x]-re[y]+3)%3!=d-1) ans++;//關係公式 
            }
            else//以前沒有記錄過關係,此話為真,所以記錄下它們的關係 
            {
                fa[fx]=fy;
                if(d==1) re[fx]=(re[y]-re[x]+3)%3;//關係公式 
                else re[fx]=(1-re[x]+re[y]+3)%3;//關係公式 
            }
        }
    }
    printf("%d",ans);
    return 0;
}


總結:對於種類並查集,先制定好種類編號,用加法的形式檢驗。合格後,繼續尋找其它的關係公式。最後,應用到並查集模版中。



通過以上兩題,有沒有發現,對於複雜些的並查集,它的find_fa函式總是有以下格式:

int find_fa(int x)
{
    if(fa[x]==x) return fa[x];
    int f=find_fa(fa[x]);
    
    //隨意幹些奇奇怪怪的事 
    
    fa[x]=f;
    return fa[x];
}

要記得先讓舊父親尋找新父親,接著處理自己的事,最後才更新自己的最新父親。因為要使舊父親的情況與最新情況同步,自己才能把與新父親的關係處理好,進而拋棄舊父親,認最新的父親。


推薦:《並查集—入門》http://blog.csdn.net/a_bright_ch/article/details/77161640


相關文章