本文著作權爲本人所有,轉載請註明出處。
正文:
談到並查集,我只想感嘆一句:
“你只看見我渺小的身軀,卻沒有看到我心中的那片森林。”
這,就是並查集思想最精妙之處.
理解下面三句話,並查集就學會了:
-
“並”的意思是把兩個處在同一個連通分量的結點給併到一起.
-
“查”的意思是查找一個結點的根節點.
-
“並”的時候需要用到“查”
步驟:
1.查(“查”的意思是查找一個結點的根節點.)
初始化一個數組,裏面存放每個節點的的父節點( 的父節點).爲什麼要這個數組呢?
數組可以表示一顆樹!
其目的是爲了查根節點,根據這個數組,我們不就可以“順藤摸瓜”,找到每個節點的根節點了嗎?
假如你在一個大家族裏,大家族中的每個人都知道自己的父親是誰,當有一天,你問你爸爸我的祖先是誰呀?你爸爸就會先問你爺爺,你爺爺就問你太爺爺,最後就能追溯到祖先 .
簡單吧,這就實現了並查集中“查”的功能.
代碼實現:
# 查根
def find(x, parent):
r = x # 假設根就是當前的結點
while r != parent[r]: # 如果假設不成立(r不是根節點),就繼續循環
r = parent[r] # 假設根節點是當前節點的父節點,即往樹的上面走一層
return r # 循環結束了,根也就找到了
爲啥這句代碼不成立的時候,就找到了根節點呢?
r != parent[r]
因爲我們在初始化的時候,每個節點的根節點初始化爲它自己,即我爸爸是我自己,這就是根節點和其他節點的不同之處!!!當 r == parent[r] 的時候,不就說明r是根節點了嗎.
# parent 數組的初始化
parent = defaultdict(int)
for i in range(len(M)):
parent[i] = i # i的爸爸是他自己
2.並(“並”的意思是把兩個處在同一個連通分量的結點給併到一起.)
並就更簡單了。
比如有兩個節點 x和y, 我們就查一下x的根節點和y的根節點(並的時候用到了查)是不是同一個節點(咱們的祖先是不是同一個人),如果是,那麼x和y本來就是一家人,不用做任何操作。
如果發現x和y的祖先不同,必須有一個人要遷移戶口,例如就讓y的祖先做x祖先的兒子,這樣x 和 y還是成爲一家人了(實現了並操縱)。
代碼:
def union(x, y, parent):
x_root = find(x, parent)
y_root = find(y, parent)
# 將x作爲根節點
if x_root != y_root:
parent[y_root] = x_root
應用到實際問題中?
這裏推薦leetcode上的一道題—《朋友圈》,供大家練習,將上面學到的知識加以運用。
鏈接:
https://leetcode-cn.com/problems/friend-circles/solution/union-find-suan-fa-xiang-jie-by-labuladong/
題目:
班上有 N 名學生。其中有些人是朋友,有些則不是。他們的友誼具有是傳遞性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那麼我們可以認爲 A 也是 C 的朋友。所謂的朋友圈,是指所有朋友的集合。
給定一個 N * N 的矩陣 M,表示班級中學生之間的朋友關係。如果M[i][j] = 1,表示已知第 i 個和 j 個學生互爲朋友關係,否則爲不知道。你必須輸出所有學生中的已知的朋友圈總數。
示例 1:
輸入:
[[1,1,0],
[1,1,0],
[0,0,1]]
輸出: 2
說明:已知學生0和學生1互爲朋友,他們在一個朋友圈。
第2個學生自己在一個朋友圈。所以返回2。
示例 2:輸入:
[[1,1,0],
[1,1,1],
[0,1,1]]
輸出: 1
說明:已知學生0和學生1互爲朋友,學生1和學生2互爲朋友,所以學生0和學生2也是朋友,所以他們三個在一個朋友圈,返回1。
解題思路用一句話概括就是:
把有朋友關係的人用union()函數合併到一起,看看合併以後還有幾個根節點,一個根節點代表一個朋友圈。
附上解答代碼:
from collections import defaultdict
# 查根
def find(x, parent):
r = x
while r != parent[r]:
r = parent[r]
return r
def union(x, y, parent):
x_root = find(x, parent)
y_root = find(y, parent)
# 將x作爲根節點
if x_root != y_root:
parent[y_root] = x_root
class Solution(object):
def findCircleNum(self, M):
"""
:type M: List[List[int]]
:rtype: int
"""
parent = defaultdict(int)
ans = set()
if not M:
return 0
for i in range(len(M)):
parent[i] = i
for i in range(len(M)):
for j in range(i, len(M[0])):
if M[i][j] == 1:
union(i, j, parent)
# 所有節點的都有哪些情況,一種情況代表一個連通分量
for i in parent:
ans.add(find(i, parent))
return len(ans)
a = Solution()
print(a.findCircleNum([[1, 1, 0], [1, 1, 0], [0, 0, 1]]))
優化
以下有部分引用自leetcode
我們一開始就是簡單粗暴的把p
所在的樹接到q
所在的樹的根節點下面,那麼這裏就可能出現「頭重腳輕」的不平衡狀況,比如下面這種局面:
長此以往,樹可能生長得很不平衡。我們其實是希望,小一些的樹接到大一些的樹下面,這樣就能避免頭重腳輕,更平衡一些。解決方法是額外使用一個size數組,記錄每棵樹包含的節點數,我們不妨稱爲「重量」:
比如說size[3] = 5
表示,以節點3
爲根的那棵樹,總共有5
個節點。
初始化代碼優化如下:
parent = defaultdict(int)
size = defaultdict(int) # size用來記錄每棵樹包含的節點數
for i in range(len(M)):
parent[i] = i
size[i] = 1 # 一開始只有一個節點,因此初始化節點數量爲1
優化後的union函數:
def union(x, y, parent,size):
x_root = find(x, parent)
y_root = find(y, parent)
if x_root != y_root:
# 誰的節點數多,誰就做根節點
if size[x_root] > size[y_root]:
parent[y_root] = x_root
size[x_root] += size[y_root]
else:
parent[x_root] = y_root
size[y_root] += size[x_root]
完整代碼:
from collections import defaultdict
# 查根
def find(x, parent):
r = x
while r != parent[r]:
r = parent[r]
return r
def union(x, y, parent,size):
x_root = find(x, parent)
y_root = find(y, parent)
# 將x作爲根節點
if x_root != y_root:
if size[x_root] > size[y_root]:
parent[y_root] = x_root
size[x_root] += size[y_root]
else:
parent[x_root] = y_root
size[y_root] += size[x_root]
class Solution(object):
def findCircleNum(self, M):
"""
:type M: List[List[int]]
:rtype: int
"""
parent = defaultdict(int)
size = defaultdict(int)
ans = set()
if not M:
return 0
for i in range(len(M)):
parent[i] = i
size[i] = 1
for i in range(len(M)):
for j in range(i, len(M[0])):
if M[i][j] == 1:
union(i, j, parent,size)
for i in parent:
ans.add(find(i, parent))
return len(ans)
a = Solution()
print(a.findCircleNum([[1, 1, 0], [1, 1, 0], [0, 0, 1]]))
總結
1.並查集的思想是很精妙的,用一個數組表示了整片森林(parent)
if M[i][j] == 1:
union(i, j, parent,size)
for i in parent:
ans.add(find(i, parent))
return len(ans)
a = Solution()
print(a.findCircleNum([[1, 1, 0], [1, 1, 0], [0, 0, 1]]))
## 總結
1.並查集的思想是很精妙的,用一個數組表示了整片森林(parent)
2.優化的關鍵在於記錄每棵樹的節點數量,讓節點數少的森林直線節點數多的森林.