Union-Find 並查集算法


一、動態連通性(Dynamic Connectivity)

Union-Find 算法(中文稱並查集算法)是解決動態連通性(Dynamic Conectivity)問題的一種算法。動態連通性是計算機圖論中的一種數據結構,動態維護圖結構中相連信息。簡單的說就是,圖中各個節點之間是否相連、如何將兩個節點連接,連接后還剩多少個連通分量。有點像我們的微信朋友圈,在社交網絡中,彼此熟悉的人之間組成自己的圈子,熟悉之后就會添加好友,加入新的圈子。微信用戶有幾億人,如何快速計算任意兩個用戶是否同屬於一個圈子呢?計算機是如何將兩個用戶連接起來的呢?整個微信用戶共有幾個獨立的圈子呢?Union-Find就可以解決上述問題。


二、基本概念
結合下面圖的例子來了解基本概念:
 
圖中8個節點都是獨立互不連通的,也就是一共有8個連通分量。

連通是一種等價關系,也就是說具有如下三個性質:

1、自反性:節點pp是連通的。

2、對稱性:如果節點pq連通,那么qp也連通。

3、傳遞性:如果節點pq連通,qr連通,那么pr也連通。

如果將節點1和節點2進行連接,那連通分量就剩余7個,如下圖:
如何在計算中實現這些操作呢?
 
class UF:
   def __init__(self,N): #N表示初始化的節點數,也即最初的連通分量數
def union(self,p,q): # 將節點p和q進行連接 def connected(self,p,q): #判斷p和q是否連接 def count(): #返回當前的連通分量

 除了社交網絡中的朋友圈計算,還可以判斷編譯器同一個變量的不同引用。

Union-Find 算法的關鍵就在於unionconnected函數的效率。使用什么樣的數據結構來實現這種高效率呢?

 

三、解決思路

用樹來表示節點直接的連接,只要是連接的節點都在同一顆樹中,多棵樹就是多個連通分量,進而組成了整個森林。怎么用森林來表示連通性呢?我們設定樹的每個節點都有一個指針指向其父節點,如果是根節點的話,這個指針指向自己。

如果某兩個節點被連通,則讓其中的(任意)一個節點的根節點接到另一個節點的根節點上,這樣,如果節點pq連通的話,它們一定擁有相同的根節點:

 

class UF:
    def __init__(self,N): #N表示初始化的節點數,也即最初的連通分量數
        self.count=N
        self.root=[0] #root表示存儲每個節點的根節點,第一個位置用0占位
        for i in range(1,N+1): #初始化每個節點的根節點指向自己
            self.root.append(i)

    def union(self,p,q):  # 將節點p和q進行連接,讓p的根節點指向q節點的根節點即可
        if self.connected(p,q):
            return;
        p_root=self.find(p)
        q_root=self.find(q)
        self.root[p_root]=q_root
        self.count-=1

    def find(self,p):     #查找節點p的根節點
        while p!=self.root[p]:
            p=self.root[p]
        return p

    def connected(self,p,q): #判斷p和q是否連接
        return self.find(p)=self.find(q)

    def count(): #返回當前的連通分量
        return self.count

 算法效率分析:

       從上述代碼可以看出,union-find算法的效率主要在於find函數上面,因為union和connected兩個函數的關鍵都在查找根節點上面,即find函數。find主要功能就是從某個節點向上遍歷到樹根,其時間復雜度就是樹的高度。我們可能習慣性地認為樹的高度就是logN,但這並不一定。logN的高度只存在於平衡二叉樹,對於一般的樹可能出現極端不平衡的情況,使得樹幾乎退化成直線鏈表,樹的高度最壞情況下可能變成N,如下圖所示:

如果按照上面的情況,左邊圈子與右邊圈子進行連接的話,每個圈子找到根節點的時間復雜度都是O(N)級別的,對於諸如社交網絡這樣數據規模巨大的問題,而unionconnected的調用都非常頻繁,每次都需要線性時間復雜度,效率就顯得比較低下了。其實這個問題就是樹不平衡造成的。

 

四、平衡樹

 造成樹不平衡的主要原因就是在節點關聯的時候,沒有考慮樹節點的多少,而是直接將p節點的根節點直接關聯到q節點的根節點上。
 
如上圖所示,第一種關聯就是不平衡樹,第二種關聯就比較好。所以改進的方法就是,在union之前,先判斷兩個樹的大小(節點數量),將小點的樹附加到大點的樹上。這樣,合並后的樹的深度不會變得非常大。要判斷樹的大小,需要引進一個新的數組,size 數組,存放樹的大小,初始化的時候 size 各元素都設為 1。
class UF:
    def __init__(self,N): #N表示初始化的節點數,也即最初的連通分量數
        self.count=N
        self.root=[0] #root表示存儲每個節點的根節點,第一個位置用0占位
        self.size=[0]
        for i in range(1,N+1): #初始化每個節點的根節點指向自己,樹的大小為1
            self.root.append(i)
            self.size.append(1)

    def union(self,p,q):  # 將節點p和q進行連接,讓p的根節點指向q節點的根節點即可
        if self.connected(p,q):
            return;
        p_root=self.find(p)
        q_root=self.find(q)
        if size[p_root]<= size[q_root]:
            self.root[p_root]=q_root
            self.size[q_root]+=self.size[p_root] #p節點數合並到q根節點上
        else:
            self.root[q_root]=self.root[p_root]
            self.size[p_root]=self.size[p_root] #q節點數合並到p根節點上
        self.count-=1

    def find(self,p):     #查找節點p的根節點
        while p!=self.root[p]:
            p=self.root[p]
        return p

    def connected(self,p,q): #判斷p和q是否連接
        return self.find(p)=self.find(q)

    def count(): #返回當前的連通分量
        return self.count

 

五、路徑壓縮(進一步優化find函數)

是不是可以進一步壓縮樹的高度,加快find函數的查找速度,find的效率提升了,等於union和connected函數效率提升了。

如果是上圖這種形式,那查找速度基本就是O(1)級別了。但是一個平衡樹一步是不可能壓縮到這種形式,可以在find函數中加上一行代碼,在每次查找的時候,就可以順便壓縮了路徑,將樹的高度進一步降低,代碼如下:

class UF:
    def __init__(self,N): #N表示初始化的節點數,也即最初的連通分量數
        self.count=N
        self.root=[0] #root表示存儲每個節點的根節點,第一個位置用0占位
        self.size=[0]
        for i in range(1,N+1): #初始化每個節點的根節點指向自己,樹的大小為1
            self.root.append(i)
            self.size.append(1)

    def union(self,p,q):  # 將節點p和q進行連接,讓p的根節點指向q節點的根節點即可
        p_root=self.find(p)
        q_root=self.find(q)
        if p_root==q_root:
            return 
        if self.size[p_root]<= self.size[q_root]:
            self.root[p_root]=q_root
            self.size[q_root]+=self.size[p_root] #p節點數合並到q根節點上
        else:
            self.root[q_root]=self.root[p_root]
            self.size[p_root]=self.size[p_root] #q節點數合並到p根節點上
        self.count-=1

    def find(self,p):     #查找節點p的根節點
        while p!=self.root[p]:
            self.root[p]=self.root[self.root[p]]#路徑壓縮,直接把p節點指向其父節點的父節點,其實查找也變成了跳躍查找了。
            p=self.root[p]
        return p

    def connected(self,p,q): #判斷p和q是否連接
        return self.find(p)==self.find(q)

    def count_func(): #返回當前的連通分量
        return self.count

這種思路每調用一次find函數,路徑就會壓縮一次,直到路徑不能壓縮為止

看代碼不好理解,我們以圖示的形式進行展示:

 
可以看出每查找一次跟節點,路徑就壓縮一次。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM