1. 程式人生 > >並查集(Union-Find)演算法詳解

並查集(Union-Find)演算法詳解

並查集(Union-Find)是解決動態連通性問題的一類非常高效的資料結構。本文中,我將盡我所能用最簡單,最清晰的邏輯展示出並查集的構造過程,同時還將對其中的關鍵步驟給出相應的Python程式碼。

動態連通性

可以想象一張地圖上有很多點,有些點之間是有道路相互聯通的,而有些點則沒有。如果我們現在要從點A走向點B,那麼一個關鍵的問題就是判斷我們能否從A走到B呢?換句話說,A和B是否是連通的。這是動態連通性最基本的訴求。現在給出一組資料,其中每個元素都是一對“點”,代表這對點之間是聯通的,我們需要設計一個演算法,讓計算機依次讀取這些資料,最後判斷出其中任意兩點是否連通。注意,並查集所涉及的動態連通性只是考慮“是否連通”這一二值判別問題,而不涉及連通的路徑到底是什麼。後者不在本文的考慮範圍之內。

舉個例子,比如下圖。為了簡單起見,我們以整數 0~9 表示圖中的10個點,然後給出兩兩連通的資料如下:[(4, 3), (3, 8), (6, 5), (9, 4), (2, 1), (8, 9), (5, 0), (7, 2), (6, 1), (6, 7)]


我們將這些“點對”依次通過畫圖連線的方式在圖中表示出來。當然,如果其中某“對”點本身就是連通的,比如點對(8,9),還有(6,7)在連線前就已經是連通的,我們可以自動忽略這種情況。通過這張圖,可以直觀的看出,有些點對,像0與1是相互連通的,而有些像0與9是不連通的。現在的問題是,如何讓計算機讀取這些資料來構建一種資料結構(合併連通點),然後在這種資料結構上高效的做出對某個點對是否連通的判斷(查詢連通性)。這也就是並查集名字的由來了。

為了實現上面所描述的功能,一個簡單的思路就是分組。也就是說,我們可以把相互連通的點看成一個組,如果現在查詢的點對分別在不同的組中,則這個點對不連通,否則連通。下面我們先來簡單分析一下這種操作的具體過程,分“並”和“查”兩個方面來分析。為了方便描述,我這裡先舉一個例子:比如上圖的10個點,現在就令每個點的值為其初始組別,我們可以得到下面這個表:

element 0 1 2 3 4 5 6 7 8 9
group number 0 1 2 3 4 5 6 7 8 9

現在,“並”的操作可以這樣來描述:觀察第一個點對(

4,3),於是先找到點4和3,發現所在組別不一樣,再將點4和3的組別都變成3(當然都變成4也行,這個隨意設計),然後就產生了如下的表:

element 0 1 2 3 4 5 6 7 8 9
group number 0 1 2 3 3 5 6 7 8 9

而“查”的操作其實就是“並”操作的第一步:找到點對中兩個點所在的組別,看是否相同。

也就是說,並查集的兩類基本操作中,都涉及了“根據點找組別”的過程,因此我們先給出一個高效的“查”的演算法。我把它叫做Quick-Find 演算法。

Quick-Find 演算法

設計高效的查詢演算法非常簡單,從上面的表格我們就能聯想到,這種元素和組別之間的一一對應關係可以用“鍵值對”的儲存方式實現。我直接給出Python的實現程式碼,非常簡單。

def con_eleGroupNum(eleList):
    """
    construct the element-group number map
    :param eleList: the list of distinct elements
    :return: the dictionary with the form {element: group number}
    """
    result = {}
    num = 1
    for i in eleList:
        result[i] = num
        num += 1
    return result

這樣,因為可以直接找到“鍵”,並通過“鍵”訪問其對應的值,所以查詢的複雜度為O(1),perfect!

但是如果是並呢?這就有點麻煩了,因為不知道到底是哪些“鍵”(點)對應著某個“值”(組別),所以需要對整個列表進行遍歷,逐一修改。假設現在有N個點,需要新增的路徑由M個點對組成,那麼“並”的時間複雜度為O(MN)。這是個平方級別的時間複雜度,顯然,在資料量極大的情況下,平方級別的演算法是有問題的。下面的程式碼展示了根據一個點對修改組別(合併)的過程,我們以每次點對元素的最小值為新的組別:

def change_GroupNum(pairGroupNum, eleGroupNum):
    """
    connect
    :param pairGroupNum: the two group numbers of the pair of elements
    :param eleGroupNum: the dictionary with the form {element: group number}
    :return: None
    """
    newGroupNum = min(pairGroupNum)
    for i in eleGroupNum:
        if eleGroupNum[i] == pairGroupNum[0] or eleGroupNum[i] == pairGroupNum[1]:
            eleGroupNum[i] = newGroupNum


def quick_find(elePair, eleGroupNum):
    """
    find and connect
    :param elePair: the pair of elements
    :param eleGroupNum: the dictionary
    :return: None
    """
    pairGroupNum = (eleGroupNum[elePair[0]], eleGroupNum[elePair[1]])
    change_GroupNum(pairGroupNum, eleGroupNum)

Quick-Union 演算法

為了解決這種低效的並,我們需要重新考慮一下這個問題。現在的關鍵在於如何能快速地通過一個點找到其相應的組別和這個組中的所有點,並且批量改變這些點的組別。顯然,這裡面設計了資料的儲存和更新,我們考慮從設計新的資料結構入手。既然簡單的鍵值對無法解決,那麼別的資料結構呢?連結串列,樹,圖?琢磨一下,你會發現樹結構其實很適合:比起連結串列和圖,樹結構有一個非常“顯眼”的根節點,我們可以用它來代表這個樹所有節點的組別,修改了根節點,就相當於是修改了全樹,此外,樹的層次化結構決定了由一個節點查詢到組別的過程也是非常高效的。

用樹結構實現並查集的演算法思路可以如下描述,假設現在要新增多個路徑(點對):

  • 初始化:每個點看做一棵樹,當然這是一棵只有根節點的樹,儲存了這個節點本身的值作為組別(你也可以令其他不會產生衝突的記號做組別);

  • 查詢:對於點對(a,b),通過ab向其根節點回溯(當然初始時就是它們本身),判斷其所在組別;

  • 合併:若不在同一組別,令其中一個點(比如a吧)所在樹的根節點成為另一個點(比如b)的根節點的孩子。這樣即便再查詢到a,通過上面的查詢過程,程式也會最終判斷得到的是現在b的根節點所在的組別,相當於是改變了a所在組的全部元素的組別;

這樣,在計算過程中的所有樹其實就是一棵多叉樹。然而我們發現,現在產生了一個新的問題,那就是因為查詢演算法也改了,導致現在Quick-Union的查詢過程效率好像不那麼令人滿意了。甚至在最壞的情況下,這樣的多叉樹退化成了一個連結串列(不具體說了,大家想想應該能明白)。

但是沒辦法啊,因為你要找樹根,怎麼著複雜度也得是O(H)的,其中H是樹高。那再想想,能不能儘可能地降低這裡的樹高呢?也就是說在產生樹合併樹的時候就儘量使得每棵樹都是“扁平化”的。

大樹小樹的合併技巧

先看看樹的合併,對於我們上面說的這種合併(一棵樹的根直接變成另一棵樹根的孩子),有一個基本的原則是小樹變成大樹的子樹,會比大樹變成小樹的子樹更加不易增加樹高,這一點通過下面的圖就能看出來。所以我們可以在生成樹的時候,令根節點儲存一個屬性 weight,用來表示這棵樹所擁有的節點數,節點數多的是“大樹”,少的就是“小樹”。


壓縮路徑

再看看樹的生成,這一點就要在查詢過程中做文章了,因為每次我們是從樹的一個節點回溯到其根節點,所以一個最直接的辦法是,將這條路徑上的所有中間節點記錄下來,全部變成根節點的子節點。但是這樣一來會增加演算法的空間複雜度(反覆開闢記憶體和銷燬)。所以一個備選的思路是每遍歷到一個節點,就將這個節點變成他的爺爺節點的孩子(和其父節點在同一層了)。相當於是壓縮了查詢的路徑,這樣,頻繁的查詢當然會導致樹的“扁平化”程度更徹底。

程式碼展示

經過上面大小樹的合併原則以及路徑的壓縮,其實“並”和“查”兩種操作的時間複雜度都非常趨近於O(1)了。下面我給出一些關鍵函式的程式碼。完整的程式碼參加我的github主頁:https://github.com/guoziqingbupt/Union-Find

class UFTreeNode(object):
    def __init__(self, num):
        # the group number
        self.num = num

        # its children
        self.children = []

        # its parent
        self.parent = None

        # the number of nodes that rooted by this node
        self.weight = 1


def genNodeList(eleList):
    """
    generating the node of each element
    :param eleList: the list of elements
    :return: a dictionary formed as {element: corresponding node}
    """
    result = {}
    for ele in eleList:
        result[ele] = UFTreeNode(ele)
    return result


def locPair(elePair, eleNodeMap):
    """
    locate the positions of the pair of elements
    :param elePair:
    :param eleNodeMap: a dictionary formed as {element: corresponding node}
    :return: the two nodes of the pair of elements
    """

    return [eleNodeMap[elePair[0]], eleNodeMap[elePair[1]]]


def backtracking(node):
    """
    1. find the root of a node
    2. cut down the height of the tree
    :param node:
    :return: the root of node
    """
    root = node

    while root.parent:
        cur = root
        root = root.parent

        # the grandfather node of cur exists
        if cur.parent.parent:

            # make the father of cur is its grandfather
            grandfather = cur.parent.parent
            grandfather.children.append(cur)

    return root


def quickUnion(elePair, eleNodeMap):
    """
    union process
    :param elePair:
    :param eleNodeMap:
    :return:
    """
    nodePair = locPair(elePair, eleNodeMap)

    root_1, root_2 = backtracking(nodePair[0]), backtracking(nodePair[1])

    # if the two elements of the pair are not belongs to the same root (group)
    if root_1 is not root_2:

        if root_1.weight >= root_2.weight:

            # update weight
            root_1.weight += root_2.weight

            # make the root2 as a subtree of root1
            root_1.children.append(root_2)

            # update the group number of root2
            root_2.num = root_1.num

        else:
            root_2.weight += root_1.weight
            root_2.children.append(root_1)
            root_1.num = root_2.num

相關推薦

(Union-Find)演算法

並查集(Union-Find)是解決動態連通性問題的一類非常高效的資料結構。本文中,我將盡我所能用最簡單,最清晰的邏輯展示出並查集的構造過程,同時還將對其中的關鍵步驟給出相應的Python程式碼。 動態連通性 可以想象一張地圖上有很多點,有些點之間是有道

(Union-Find)演算法

本文轉載自csdn另一博主,其原文點這裡。 public int find(int[] parent, int i) { if (parent[i] != i) { parent[i] = find(parent, pare

資料結構與演算法(十二)(Union Find)

本文主要包括以下內容: 並查集的概念 並查集的操作 並查集的實現和優化 Quick Find Quick Union 基於size的優化 基於rank的優化 路徑壓縮優化 並查集的時間複雜度 並查集的概念 在電腦科學中,並查集 是一種樹形的資料結

演算法入門---java語言實現的(Union-Find)小結

圖片來自慕課網,僅僅為了記錄學習。 基本概念 /** * * 並查集,用來解決連通問題的,兩個節點之間是否是連通的。 * 此處的節點是抽象的概念:比如使用者和使用者之間,港口和港口之間。

POJ 1611 The Suspects Union Find

subset oid fin 由於 urn data tracking -m cts 本題也是個標準的並查集題解。 操作完並查集之後,就是要找和0節點在同一個集合的元素有多少。 註意這個操作,須要先找到0的父母節點。然後查找有多少個節點的額父母節點和0的父母節點同樣。

(Union-Find)

數組 樹根 情況 由於 指針 oid 父節點 要求 基本 一、基本操作: 1、Find:當且僅當兩個元素屬於相同的集合時,返回相同的名字 2、Union:將兩個不相交的集合合並為一個不想交的集合。 應用:在等價關系中,判斷兩個元素之間是否有關系或者添加等價關系。 二、基本數

【LeetCode】 union-find(共16題)

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica } 【128】Longest Consecutive Sequence  【130】Surrounded Regions  【200】Number of Is

資料結構——Union Find

一、並查集解決了什麼問題? 1、網路中節點間的連線狀態:這裡的網路是一個抽象的概念,指的是使用者之間形成的網路 2、兩個或兩個以上集合之間的交集 二、對並查集的設計 對於一組資料,主要支援兩個操作 public interface UnionFind {

(Union-Find Sets)及其應用

並查集 (Union-Find Sets)並查集:(union-find sets)是一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多。一般採取樹形結構來儲存並查集,並利用一個rank陣列來儲存集合的深度下界,在查詢操

(Union-Find) 應用舉例

本文是作為上一篇文章 《並查集演算法原理和改進》 的後續,焦點主要集中在一些並查集的應用上。材料主要是取自POJ,HDOJ上的一些演算法練習題。 首先還是回顧和總結一下關於並查集的幾個關鍵點: 以樹作為節點的組織結構,結構的形態很是否採取優化策略有很大關係,未進行優化的樹結構可能會是“畸形”樹(嚴重不

(Union-Find)粗略介紹

並查集:一種樹型的資料結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。常常在使用中以森林來表示。 集:就是讓每個元素構成一個單元素的集合,也就是按一定順序將屬於同一組的元素所在的集合合併。 例子:為了解釋並查集的原理,我將

一、 (Union-Find Set)

如果:給出各個元素之間的聯絡,要求將這些元素分成幾個集合,每個集合中的元素直接或間接有聯絡。在這類問題中主要涉及的是對集合的合併和查詢,因此將這種集合稱為並查集。  連結串列被普通用來計算並查集.表中的每個元素設兩個指標:一個指向同一集合中的下一個元素;另一個指向表首元素。  鏈結構的並查集  採用鏈式儲

(Union-Find Sets)及其應用

並查集 (Union-Find Sets) 並查集:(union-find sets)是一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多。一般採取樹形結構來儲存並查集,並利用一個rank陣列來儲存集合的深度下界,在查

(Union-Find Algorithm),看這一篇就夠了

動態連線(Dynamic connectivity)的問題 所謂的動態連線問題是指在一組可能相互連線也可能相互沒有連線的物件中,判斷給定的兩個物件是否聯通的一類問題。這類問題可以有如下抽象: 有一組構成不相交集合的物件 union: 聯通兩個物件

(union-find)模板

#include<cstdio> #include<cstring> #include<vector> using namespace std; const int mx = 100005; int fa[mx], rk[mx]; ve

(Union-Find

在電腦科學中,並查集是一種樹型的資料結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題。有一個聯合-查詢演算法(union-find algorithm)定義了兩個用於此資料結構的操

(Union-Find) 應用舉例 --- 基礎篇

本文是作為上一篇文章 《並查集演算法原理和改進》 的後續,焦點主要集中在一些並查集的應用上。材料主要是取自POJ,HDOJ上的一些演算法練習題。 首先還是回顧和總結一下關於並查集的幾個關鍵點: 以樹作為節點的組織結構,結構的形態很是否採取優化策略有很大關係,未進行優化的

POJ 1182 食物鏈 [資料結構- union-find sets]

   在輸入時可以先判斷題目所說的條件2和3,即:       1>若(x>n||y>n):即當前的話中x或y比n大,則假話數目num加1.       2>若(x==2&&x==y):即當前的話表示x吃x,則假話數目num加1.   而不屬於這兩種情況外的話語要利用

(不相交集合)與java實現

目錄 認識並查集 並查集解析 基本思想 如何檢視a,b是否在一個集合? a,b合併,究竟是a的祖先合併在b的祖先上,還是b的祖先合併在a上? 其他路徑壓縮?

Find函式

發現自己寫了兩道題都把Find函式寫得一塌糊塗。。。一題是沒有路徑壓縮過不了,一題是因為前面寫錯過弄混了,不三不四的。。。總結很重要啊!! 路徑壓縮:遞迴寫法: int Find (int x) {    if(Father[x] != x)     {     Fathe