並查集與十度好友

一、問題簡介

    首先說下問題吧,這裏就不做廣告了,在社交網絡中,假設A和B是好友,B和C是好友,如果A和C不是好友,那麼就說C是A的二度好友,在一個有10萬人人際網絡中,如何在時間O(n)時間裏,找到某個人的十度好友。

    查了下網上人們對這個問題解法的思路,大致有兩種觀點:(1)BFS;(2)並查集。對BFS解法的解釋挺多,思路都比較相近,現總結如下:

    (1)設要查找十度好友的人爲A,則首先通過BFS對A的鄰接好友進行遍歷,並定義一個變量degree記錄當前BFS的深度;

    (2)設置一個集合prefriend用來記錄在BFS過程中A的一度至九度的所有好友集合prefriend,然後找到十度好友後要剔除那些存在於prefriend中的好友,設置prefriend的原因是集合中如果有環,則會出現如果X是A的三度好友,同時X也有可能是A的十度好友;

    (3)當degree==10時,從當前BFS的集合中再剔除prefriend集合中的好友,則剩下的好友便爲A的十度好友了。

    採用BFS時間複雜度是BFS的複雜度O(V+E)。當集合中邊很多時,可能複雜度接近於二次複雜度。

    網上對並查集的思路僅僅是提筆帶過,這裏較詳細的介紹下並查集的基本概念和對此問題解法的思路,體現下並查集這一單純又強大的算法的神奇。

二、並查集

    一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多,如其求無向圖的連通分量個數等。最完美的應用當屬:實現Kruskar算法求最小生成樹。

    實現並查集主要實現其三個基本操作:

    (1)Make_Set(x) 把每一個元素初始化爲一個集合,初始化後每一個元素的父親節點是它本身,每一個元素的祖先節點也是它本身。

    (2)Find_Set(x) 查找一個元素所在的集合,查找一個元素所在的集合,即找到這個元素所在集合的祖先!這個纔是並查集判斷和合並的依據。判斷兩個元素是否屬於同一集合,只要看他們所在集合的祖先是否相同即可。

    (3)Union(x,y) 合併x,y所在的兩個集合,合併兩個集合,也就是使一個集合的祖先成爲另一個集合的祖先,具體見下圖所示:


三、並查集的優化

    (1)Find_Set(x)時 路徑壓縮

    尋找祖先時我們一般採用遞歸查找,但是當元素很多亦或是整棵樹變爲一條鏈時,每次Find_Set(x)都是O(n)的複雜度,這裏我們採用路徑壓縮,即當我們經過遞歸地尋找祖先節點時,在"回溯"的時候順便將它的子孫節點都直接指向祖先,這樣以後再次Find_Set(x)時複雜度就變成O(1)了,如下圖所示;可見,路徑壓縮方便了以後的查找。


    (2)Union(x,y)時 按秩合併

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

四、代碼實現

int father[MAX];   /* father[x]表示x的父節點*/
int rank[MAX];     /* rank[x]表示x的秩*/
 
/* 初始化集合*/
 
void Make_Set(int x)
{
    father[x] = x; //根據實際情況指定的父節點可變化
    rank[x] = 0;   //根據實際情況初始化秩也有所變化
}
 
/* 查找x元素所在的集合,回溯時壓縮路徑*/
 
int Find_Set(int x)
{
    if (x != father[x])
    {
        father[x] = Find_Set(father[x]); //這個回溯時的壓縮路徑是精華
    }
    return father[x];
}
 
/*
按秩合併x,y所在的集合
下面的那個if else結構不是絕對的,具體<strong>根據實際情況</strong>變化
但是,宗旨是不變的即,按秩合併,實時更新秩。
*/
 
void Union(int x, int y)
{
    x = Find_Set(x);
    y = Find_Set(y);
    if (x == y) return;
    if (rank[x] > rank[y])
    {
        father[y] = x;
        rank[x] += rank[y];
    }else
    {
        if (rank[x] == rank[y])
        {
            rank[y]++;
        }
        father[x] = y;
    }
}
    注意:代碼中路徑壓縮時秩不需變化的,秩只是表示節點高度的一個上界,如果用秩進行計數,路徑壓縮也是不需要變化的,因爲所屬集合的根節點的秩在合併時已經更新,其他子節點的秩不用到也無需再變化;在某些應用中,可根據問題的不同來定義表示不同含義的數據體現集合的大小等屬性。

五、並查集解決十度好友問題

    下面說下使用並查集解決十度好友問題的基本思路:

    (1)設數據集合中的數據時以鄰接表的形式存儲的,如1->2->4->6->7,表示編號1的好友的鄰接表,以此類推;

    (2)同樣設要查找十度好友的人爲A,合併A的一度好友,即1、2、4、6、7,這些好友的根節點都相同,爲了便於計算,我們應該維護一條記錄,記錄每次新加入集合S的節點集合NFriend(NFriend中每個點其實就是第N度好友的集合);

    (2)針對NS中每個點的好友集合進行合併,當然合併的過程中只需對要合併的點i進行判斷findSet(i)是否屬於集合S,如果屬於,則不用加入集合NFriend了;

    (3)最後十步合併後,NFriend便是所求的集合。

發佈了61 篇原創文章 · 獲贊 19 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章