并查集

(本來(lái)想寫個(gè)并查集的文章灯帮,發(fā)現(xiàn)這一篇寫得很好琅绅,就直接摘抄過(guò)來(lái)了召川,也做個(gè)記錄【union-find】霞丧。

1. 概述

并查集是一種樹型的數(shù)據(jù)結(jié)構(gòu)科吭,用于處理一些不交集(Disjoint Sets)的合并及查詢問(wèn)題。有一個(gè)聯(lián)合-查找算法(Union-find Algorithm)定義了兩個(gè)用于此數(shù)據(jù)結(jié)構(gòu)的操作:

find:確定元素屬于哪一個(gè)子集浑此。它可以被用來(lái)確定兩個(gè)元素是否屬于同一子集累颂。
union:將兩個(gè)子集合并成同一個(gè)集合滞详。
初始化每一個(gè)點(diǎn)都是一個(gè)連通域凛俱,類似下圖:



由于支持這兩種操作,一個(gè)不相交集也常被稱為聯(lián)合-查找數(shù)據(jù)結(jié)構(gòu)(Union-find Data Structure)或合并-查找集合(Merge-find Set)料饥。為了更加精確的定義這些方法蒲犬,需要定義如何表示集合。一種常用的策略是為每個(gè)集合選定一個(gè)固定的元素岸啡,稱為代表原叮,以表示整個(gè)集合。接著巡蘸,find(x) 返回 x 所屬集合的代表奋隶,而 union 使用兩個(gè)集合的代表作為參數(shù)。

2. 核心API

2.1 find

    def find(self, x):
        # 未進(jìn)行路徑壓縮
        while x != self.parent[x]:
            x = self.parent[x]
        return x

比如下面這個(gè)圖悦荒,調(diào)用 find(0) 會(huì)逐步找到 3 唯欣。


若采用路徑壓縮方法,在找到 3 的過(guò)程上會(huì)將路徑上的節(jié)點(diǎn)都指向根節(jié)點(diǎn)搬味。


image
    def find(self, x):
        """
        查找節(jié)點(diǎn)的父(根)節(jié)點(diǎn)
        """
        if x != self.parent[x]:
            # 進(jìn)行了路徑壓縮
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

極限情況下境氢,每一個(gè)路徑都會(huì)被壓縮蟀拷,這種情況下繼續(xù)查找的時(shí)間復(fù)雜度就是 O(1)

2.2 union

將其中一個(gè)節(jié)點(diǎn)掛到另外一個(gè)節(jié)點(diǎn)的祖先上萍聊,這樣兩者祖先就一樣了问芬。也就是說(shuō),兩個(gè)節(jié)點(diǎn)聯(lián)通了寿桨。

對(duì)于如下的一個(gè)圖:



圖中 r:1 表示 秩為 1此衅,r 是 rank 的簡(jiǎn)寫。記錄以該結(jié)點(diǎn)為根結(jié)點(diǎn)的樹的深度牛隅。

    def union(self, x, y):
        """
        合并兩個(gè)節(jié)點(diǎn)炕柔,小的樹掛到大的樹上,使樹盡量平衡
        """
        p, q = self.find(x), self.find(y)
        if p == q:
            return
        if self.rank[p] < self.rank[q]:
            self.parent[p] = q
        else:
            self.parent[q] = p
        if self.rank[p] == self.rank[q]:
            self.rank[p] += 1
        self.count -= 1

如果我們將 0 和 7 進(jìn)行一次合并媒佣。即 union(0, 7) 匕累,則會(huì)發(fā)生如下過(guò)程。


3. 不帶權(quán)并查集

完整代碼如下:


class UnionFind:

    def __init__(self, n):
        """
        并查集
        :param n: 節(jié)點(diǎn)個(gè)數(shù)
        """
        # 記錄父節(jié)點(diǎn)
        self.parent = {}
        # 記錄以該結(jié)點(diǎn)為根結(jié)點(diǎn)的樹的深度
        self.rank = {}
        # 記錄連通分量的個(gè)數(shù)
        self.count = 0
        # 初始化
        for i in range(n):
            self.parent[i] = I
            self.rank[i] = 1
            self.count += 1

    def find(self, x):
        """
        查找節(jié)點(diǎn)的父(根)節(jié)點(diǎn)
        """
        if x != self.parent[x]:
            # 進(jìn)行了路徑壓縮
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        """
        合并兩個(gè)節(jié)點(diǎn)默伍,小的樹掛到大的樹上欢嘿,使樹盡量平衡
        """
        p, q = self.find(x), self.find(y)
        if p == q:
            return
        if self.rank[p] < self.rank[q]:
            self.parent[p] = q
        else:
            self.parent[q] = p
        if self.rank[p] == self.rank[q]:
            self.rank[p] += 1
        self.count -= 1

4. 帶權(quán)并查集

實(shí)際上并查集就是圖結(jié)構(gòu),我們使用了哈希表來(lái)模擬這種圖的關(guān)系也糊。 而上面講到的其實(shí)都是有向無(wú)權(quán)圖炼蹦,因此僅僅使用 parent 表示節(jié)點(diǎn)關(guān)系就可以了。而如果使用的是有向帶權(quán)圖呢狸剃?實(shí)際上除了維護(hù) parent 這樣的節(jié)點(diǎn)指向關(guān)系掐隐,我們還需要維護(hù)節(jié)點(diǎn)的權(quán)重,一個(gè)簡(jiǎn)單的想法是使用另外一個(gè)哈希表 weight 存儲(chǔ)節(jié)點(diǎn)的權(quán)重關(guān)系钞馁。比如 weight[a] = 1 表示 a 到其父節(jié)點(diǎn)的權(quán)重是 1虑省。

如果是帶權(quán)的并查集,其查詢過(guò)程的路徑壓縮以及合并過(guò)程會(huì)略有不同僧凰,因?yàn)槲覀儾粌H關(guān)心節(jié)點(diǎn)指向的變更探颈,也關(guān)心權(quán)重如何更新。比如:

a    b
^    ^
|    |
|    |
x    y

如上表示的是 x 的父節(jié)點(diǎn)是 a训措,y 的父節(jié)點(diǎn)是 b伪节,現(xiàn)在我需要將 x 和 y 進(jìn)行合并。

a    b
^    ^
|    |
|    |
x -> y

假設(shè) x 到 a 的權(quán)重是 w(xa)绩鸣,y 到 b 的權(quán)重為 w(yb)怀大,x 到 y 的權(quán)重是 w(xy)。合并之后會(huì)變成如圖的樣子:

a -> b
^    ^
|    |
|    |
x    y

那么 a 到 b 的權(quán)重應(yīng)該被更新為什么呢呀闻?我們知道 w(xa) + w(ab) = w(xy) + w(yb)化借,也就是說(shuō) a 到 b 的權(quán)重 w(ab) = w(xy) + w(yb) - w(xa)。

當(dāng)然上面關(guān)系式是加法总珠,減法屏鳍,取模還是乘法勘纯,除法等完全由題目決定,我這里只是舉了一個(gè)例子钓瞭。不管怎么樣驳遵,這種運(yùn)算一定需要滿足可傳導(dǎo)性。

完整代碼如下:

class WeightedUnionFind:

    def __init__(self, n):
        """
        :param n: 節(jié)點(diǎn)個(gè)數(shù)
        """
        # 記錄父節(jié)點(diǎn)
        self.parent = {}
        # 記錄到父節(jié)點(diǎn)的權(quán)重
        self.weight = {}
        for i in range(n):
            self.parent[i] = I
            self.weight[i] = 0

    def find(self, x):
        if x != self.parent[x]:
            p, w = self.find(self.parent[x])
            self.parent[x] = p
            self.weight[x] += w
        return self.parent[x], self.weight[x]

    def union(self, x, y, w):
        p_x, w_x = self.find(x)
        p_y, w_y = self.find(y)
        if p_x == p_y:
            return
        self.parent[p_x] = p_y
        self.weight[p_x] = w + w_y - w_x

5. leetcode

721. 賬戶合并

1202. 交換字符串中的元素

1697. 檢查邊長(zhǎng)度限制的路徑是否存在

399. 除法求值

參考文獻(xiàn)

  1. https://github.com/azl397985856/leetcode/blob/master/thinkings/union-find.md
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末山涡,一起剝皮案震驚了整個(gè)濱河市堤结,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鸭丛,老刑警劉巖竞穷,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鳞溉,居然都是意外死亡瘾带,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門熟菲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)看政,“玉大人,你說(shuō)我怎么就攤上這事抄罕≡黍迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵呆贿,是天一觀的道長(zhǎng)嚷兔。 經(jīng)常有香客問(wèn)我,道長(zhǎng)做入,這世上最難降的妖魔是什么冒晰? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮母蛛,結(jié)果婚禮上翩剪,老公的妹妹穿的比我還像新娘乳怎。我一直安慰自己彩郊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布蚪缀。 她就那樣靜靜地躺著秫逝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪询枚。 梳的紋絲不亂的頭發(fā)上违帆,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音金蜀,去河邊找鬼刷后。 笑死的畴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尝胆。 我是一名探鬼主播丧裁,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼含衔!你這毒婦竟也來(lái)了煎娇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贪染,失蹤者是張志新(化名)和其女友劉穎缓呛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杭隙,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哟绊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痰憎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匿情。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖信殊,靈堂內(nèi)的尸體忽然破棺而出炬称,到底是詐尸還是另有隱情,我是刑警寧澤涡拘,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布玲躯,位于F島的核電站,受9級(jí)特大地震影響鳄乏,放射性物質(zhì)發(fā)生泄漏跷车。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一橱野、第九天 我趴在偏房一處隱蔽的房頂上張望朽缴。 院中可真熱鬧,春花似錦水援、人聲如沸密强。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)或渤。三九已至,卻和暖如春奕扣,著一層夾襖步出監(jiān)牢的瞬間薪鹦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留池磁,地道東北人奔害。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像地熄,于是被迫代替她去往敵國(guó)和親舀武。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容