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

連通是一種等價關系,也就是說具有如下三個性質:
1、自反性:節點p
和p
是連通的。
2、對稱性:如果節點p
和q
連通,那么q
和p
也連通。
3、傳遞性:如果節點p
和q
連通,q
和r
連通,那么p
和r
也連通。

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 算法的關鍵就在於union
和connected
函數的效率。使用什么樣的數據結構來實現這種高效率呢?
三、解決思路
用樹來表示節點直接的連接,只要是連接的節點都在同一顆樹中,多棵樹就是多個連通分量,進而組成了整個森林。怎么用森林來表示連通性呢?我們設定樹的每個節點都有一個指針指向其父節點,如果是根節點的話,這個指針指向自己。
如果某兩個節點被連通,則讓其中的(任意)一個節點的根節點接到另一個節點的根節點上,這樣,如果節點p
和q
連通的話,它們一定擁有相同的根節點:
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)級別的,對於諸如社交網絡這樣數據規模巨大的問題,而union
和connected
的調用都非常頻繁,每次都需要線性時間復雜度,效率就顯得比較低下了。其實這個問題就是樹不平衡造成的。
四、平衡樹

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函數,路徑就會壓縮一次,直到路徑不能壓縮為止。
看代碼不好理解,我們以圖示的形式進行展示:
