1. 程式人生 > >並查集(Union-Find Set)

並查集(Union-Find Set)

並查集的三種操作 

(1)Union(Root1, Root2):把子集合Root2併入集合Root1中。要求這兩個集合互不相交,否則不執行合併。 (2)Find(x):搜尋單元素x所在的集合,並返回該集合的名字。 (3)UnionFindSets(s):建構函式,將並查集中s個元素初始化為s個只有一個單元素的子集合。 --------------------- 本文來自 razor_edge 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/weixin_37818081/article/details/78633187?utm_source=copy

我們可以把每個連通的分量看成一個集合,該集合包含了連通分量中的所有點。這些點兩兩連通,而具體的連通方式無關緊要,就好比集合中的元素沒有先後順序之分,只有屬於和不屬於的區別。在圖中,每個點恰好屬於一個連通分量,對應到集合表示中,每個元素恰好屬於一個集合。換句話說,圖的所有連通分量可以用若干不相交集合來表示。

並查集的精妙之處在於用樹來表示集合。例如,若包含點1,2,3,4,5,6的圖有3個連通分量{1,3},{2,5,6},{4},則需要用三棵樹來表示。這三棵樹的具體形態無關緊要,只要有一棵樹包含1,3兩個點,一棵樹包含2,5,6這三個點,還有一棵樹只包含4這一個點即可。我們規定每棵樹的根節點是這棵樹所對應的集合的代表元(representative)。

如果把x的父節點儲存在p[x]中(如果x沒有父親,則p[x]等於x),則不難寫出“查詢節點x所在數的根節點”的遞迴程式:

int find(int x){p[x]==x?x:find(p[x]);}

翻譯成大白話就是:如果p[x]等於x,說明x本身就是樹根,因此返回x;否則返回x的父親p[x]所在樹的樹根。

問題來了:在特殊情況下,這棵樹可能是一條長長的鏈。設鏈的最後一個結點為x,則每次執行find(x)都會遍歷整條鏈,效率十分低下。看上去是個棘手的問題,其實改進方法很簡單。既然每棵樹表示的只是一個集合,因此樹的形態不變,只要順便把遍歷過的結點都改成樹根的兒子,下次查詢就會快很多了,如圖所示:

並查集中的路徑壓縮

這樣,Kruskal演算法的完整程式碼便不難給出了。假設第i條邊的兩個端點序號和權值分別儲存在u[i],v[i]和w[i]中,而排序後第i小的邊的序號儲存在r[i]中(順便說一句,這叫做間接排序——排序的關鍵字是物件的“代號”,而不是物件本身)。

int cmp(const int i, const int j) {return w[i]<w[j]; } //間接排序函式
int find(int x ){ return p[x]==x?x:p[x]=find(p[x]); }//並查集的find
int Kruskal()
{
    int ans=0;
    for(int i=0;i<n;i++) p[i]=i; //初始化並查集
    for(int i=0;i<m;i++) r[i]=i; //初始化邊序號
    sort(r,r+m,cmp); //給邊排序
    for(int i=0;i<m;i++)
    {
        int e=r[i]; int x=find(u[e]); int y=find(v[e]); //找出當前邊兩個端點所在集合的編號
        if(x!=y) {ans+=w[e];p[x]=y;} //如果在不同集合,合併 -----問題①
    }
    return ans;
}

注意問題①部分不能寫成p[u[e]]=p[v[e]],因為u[e]和v[e]不一定是樹根(執行的find之後,才行)