通俗易懂地講解《並查集》

本文著作權爲本人所有,轉載請註明出處。

正文:

談到並查集,我只想感嘆一句:

“你只看見我渺小的身軀,卻沒有看到我心中的那片森林。”

這,就是並查集思想最精妙之處.

理解下面三句話,並查集就學會了:

  • “並”的意思是把兩個處在同一個連通分量的結點給併到一起.

  • “查”的意思是查找一個結點的根節點.

  • “並”的時候需要用到“查”

步驟:

1.查(“查”的意思是查找一個結點的根節點.

初始化一個parentparent數組,裏面存放每個節點的的父節點parent[i]=iparent[i] = i 的父節點).爲什麼要這個數組呢?

parentparent 數組可以表示一顆樹!

其目的是爲了查根節點,根據這個數組,我們不就可以“順藤摸瓜”,找到每個節點的根節點了嗎?

假如你在一個大家族裏,大家族中的每個人都知道自己的父親是誰,當有一天,你問你爸爸我的祖先是誰呀?你爸爸就會先問你爺爺,你爺爺就問你太爺爺,最後就能追溯到祖先 rootroot.

mark

簡單吧,這就實現了並查集中“查”的功能.

代碼實現:

# 查根
def find(x, parent):
    r = x # 假設根就是當前的結點
    while r != parent[r]: # 如果假設不成立(r不是根節點),就繼續循環
        r = parent[r] # 假設根節點是當前節點的父節點,即往樹的上面走一層
    return r # 循環結束了,根也就找到了

爲啥這句代碼不成立的時候,就找到了根節點呢?

r != parent[r]

因爲我們在初始化的時候,每個節點的根節點初始化爲它自己,即我爸爸是我自己,這就是根節點和其他節點的不同之處!!!當 r == parent[r] 的時候,不就說明r是根節點了嗎.

mark

mark

# 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還是成爲一家人了(實現了並操縱)。

mark

代碼:

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所在的樹的根節點下面,那麼這裏就可能出現「頭重腳輕」的不平衡狀況,比如下面這種局面:

mark

長此以往,樹可能生長得很不平衡。我們其實是希望,小一些的樹接到大一些的樹下面,這樣就能避免頭重腳輕,更平衡一些。解決方法是額外使用一個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.優化的關鍵在於記錄每棵樹的節點數量,讓節點數少的森林直線節點數多的森林.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章