朋友圈問題
現在有 105個使用者,編號為 1- 105。已知有 m 對關係,每一對關係給你兩個數 x 和 y ,代表編號為 x 的使用者和編號為 y 的使用者是在一個圈子中,例如: A 和 B 在一個圈子中, B 和 C 在一個圈子中,那麼 A , B , C 就在一個圈子中。現在想知道最多的一個圈子內有多少個使用者。
資料範圍:1<= m <= 2 * 10 6 。
進階:空間複雜度 O(n),時間複雜度 O(nlogn)。
輸入描述:
第一行輸入一個整數T,接下來有T組測試資料。對於每一組測試資料:第一行輸入1個整數n,代表有n對關係。接下來n行,每一行輸入兩個數x和y,代表編號為x和編號為y的使用者在同一個圈子裡。
1 ≤ T ≤ 10
1 ≤ n ≤ 2 * 106
1 ≤ x, y ≤ 105
輸出描述:
對於每組資料,輸出一個答案代表一個圈子內的最多人數。
示例:
輸入:
2
4
1 2
3 4
5 6
1 6
4
1 2
3 4
5 6
7 8
輸出:
4
2
分析問題
通過分析題目,我們可以知道,這道題是求元素分組的問題,即將所有使用者分配到不相交的圈子中,然後求出所有圈子中人數最多的那個圈子。
很顯然,我們可以使用並查集來求解。
首先,我們來看一下什麼是並查集。
並查集是用來將一系列的元素分組到不相交的集合中,並支援合併和查詢操作。
- 合併(Union):把兩個不相交的集合合併為一個集合。
- 查詢(Find):查詢兩個元素是否在同一個集合中。
並查集的重要思想在於,用集合中的一個元素代表集合。
理論總是過於抽象化,下面我們通過一個例子來說明並查集是如何運作的。
我們這裡把集合比喻成幫派,而集合中的代表就是幫主。
一開始,江湖紛爭四起,所有大俠各自為戰,他們每個人都是自己的幫主(對於只有一個元素的集合,代表元素自然就是唯一的那個元素)。
有一天,江湖人士張三和李四偶遇,都想把對方招募到麾下,於是他們進行了一場比武,結果張三贏了,於是把李四招募到了麾下,那麼李四的幫主就變成了張三(合併兩個集合,幫主就是這個集合的代表元素)。
然後,李四又和王五偶遇,兩個人互相不服,於是他們進行了一場比武,結果李四又輸了(李四怎麼那麼菜呢),此時李四能乖乖認慫,加入王五的幫派嗎?那當然是不可能!! 此時的李四已經不再是一個人在戰鬥,於是他呼叫他的老大張三來,張三聽說小弟被欺負了,那必須收拾他!!於是和王五比試了一番,結果張三贏了,然後把王五也拉入了麾下(其實李四沒必要和王五比試,因為李四比較慫,直接找大哥來收拾王五即可)。此時王五的幫主也是張三了。
我們假設張三二,李四二也進行了幫派的合併,江湖局勢變成了如下的樣子,形成了兩大幫派。
通過上圖,我們可以知道,每個幫派(一個集合)是一個樹狀的結構。
要想尋找到集合的代表元素(幫主),只需要一層層往上訪問父節點,直達樹的根節點即可。其中根節點的父節點是它自己。
採用這個方法,我們就可以寫出最簡單版本的並查集程式碼。
-
初始化
我們用陣列 fa 來儲存每個元素的父節點(這裡每個元素有且只有一個父節點)。一開始,他們各自為戰,我們將它們的父節點設為自己(假設目前有編號為1~n的n個元素)。
def __init__(self,n): self.fa=[0]*(n+1) for i in range(1,n+1): self.fa[i]=i
-
查詢
這裡我們使用遞迴的方式查詢某個元素的代表元素,即一層一層的訪問父節點,直至根節點(根節點是指其父節點是其本身的節點)。
def find(self,x): if self.fa[x]==x: return x else: return self.find(self.fa[x])
-
合併
我們先找到兩個元素的根節點,然後將前者的父節點設為後者即可。當然也可以將後者的父節點設為前者,這裡暫時不重要。後面會給出一個更合理的比較方法。
def merge(self,x,y): x_root=self.find(x) y_root=self.find(y) self.fa[x_root]=y_root
整體程式碼如下所示。
class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
def find(self,x):
if self.fa[x]==x:
return x
else:
return self.find(self.fa[x])
def merge(self,x,y):
x_root=self.find(x)
y_root=self.find(y)
self.fa[x_root]=y_root
優化
上述最簡單的並查集程式碼的效率比較低。假設目前的集合情況如下所示。
此時要呼叫merge(2,4)函式,於是從2找到1,然後執行f[1]=4,即此時的集合情況變成如下形式。
然後我們執行merge(2,5)函式,於是從2找到1,然後找到4,最後執行f[4]=5,即此時的集合情況變成如下形式。
一直執行下去,我們就會發現該演算法可能會形成一條長長的鏈,隨著鏈越來越長,我們想要從底部找到根節點會變得越來越難。
所以就需要進行優化處理,這裡我們可以使用路徑壓縮的方法,即使每個元素到根節點的路徑儘可能的短。
具體來說,我們在查詢的過程中,把沿途的每個節點的父節點都設定為根節點即可。那麼下次再查詢時,就可以很簡單的獲取到元素的根節點了。程式碼如下所示:
def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
經過路徑壓縮後,並查集程式碼的時間複雜度已經很低了。
下面我們再來進一步的進行優化處理---按秩合併。
這裡我們需要先說明一點,因為路徑壓縮優化只是在查詢時進行的,也只能壓縮一條路徑,因此經過路徑優化後,並查集最終的結構仍然可能是比較複雜的。假設,我們現在有一顆比較複雜的樹和一個元素進行合併操作。
如果此時我們要merge(1,6),我們應該把6的父節點設為1。因為如果把1的父節點設為6,會使樹的深度加深,這樣就會使樹中的每個元素到根節點的距離都變長了,從而使得之後我們尋找根節點的路徑也就會相應的變長。而如果把6的父節點設為1,就不會出現這個問題。
這就啟發我們應該把簡單的樹往復雜的樹上去合併,因為這樣合併後,到根節點距離變長的節點個數比較少。
具體來說,我們用一個陣列rank 來記錄每個根節點對應的樹的深度(如果對應元素不是樹的根節點,其rank值相當於以它作為根節點的子樹的深度)。
初始時,把所有元素的rank設為1。在合併時,比較兩個根節點,把rank較小者往較大者上合併。
下面我們來看一下程式碼的實現。
def merge(self,x,y):
#找個兩個元素對應的根節點
x_root=self.find(x)
y_root=self.find(y)
if self.rank[x_root] <= self.rank[y_root]:
self.fa[x_root]=y_root
else:
self.fa[y_root] = x_root
#如果深度相同且根節點不同,則新的根節點的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1
所以,我們終極版的並查集程式碼如下所示。
class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
self.rank=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
self.rank[i]=i
def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
def merge(self,x,y):
#找個兩個元素對應的根節點
x_root=self.find(x)
y_root=self.find(y)
if self.rank[x_root] <= self.rank[y_root]:
self.fa[x_root]=y_root
else:
self.fa[y_root] = x_root
#如果深度相同且根節點不同,則新的根節點的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1
有了並查集的思想,那我們這道朋友圈的問題就迎刃而解了。下面我們給出可以AC的程式碼。
class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
self.rank=[0]*(n+1)
self.node_num=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
self.rank[i]=1
self.node_num[i]=1
def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
def merge(self,x,y):
#找個兩個元素對應的根節點
x_root=self.find(x)
y_root=self.find(y)
if self.rank[x_root] <= self.rank[y_root]:
#將x_root集合合併到y_root上
self.fa[x_root]=y_root
self.node_num[y_root] = self.node_num[y_root] + self.node_num[x_root]
else:
#將y_root集合合併到x_root上
self.fa[y_root] = x_root
self.node_num[x_root] = self.node_num[x_root] + self.node_num[y_root]
#如果深度相同且根節點不同,則新的根節點的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1
if __name__ == '__main__':
#最多有N個使用者
N=100000
result=[]
T = int(input("請輸入多少組檢測資料?"))
while T>0:
n = int(input("輸入多少對使用者關係"))
print("輸入{}組使用者關係".format(n))
s1=Solution(N)
for i in range(n):
cur=input()
cur_users=cur.split(" ")
s1.merge(int(cur_users[0]), int(cur_users[1]))
max_people=1
for i in range(len(s1.node_num)):
max_people=max(max_people, s1.node_num[i])
result.append(max_people)
T=T-1
for x in result:
print(x)
到此,我們的並查集就聊完了。
囉嗦一句
現在給出一個思考題,可以把你的思考寫在留言區。
現在給出某個親戚關係圖,判斷任意給出的兩個人是否具有親戚關係。
原創不易!各位小夥伴覺得文章不錯的話,不妨點贊(在看)、留言、轉發三連走起!
你知道的越多,你的思維越開闊。我們下期再見。