日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

并查集(Union-Find)算法介绍

發(fā)布時(shí)間:2025/6/15 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并查集(Union-Find)算法介绍 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文主要介紹解決動(dòng)態(tài)連通性一類問(wèn)題的一種算法,使用到了一種叫做并查集的數(shù)據(jù)結(jié)構(gòu),稱為Union-Find

更多的信息可以參考Algorithms?一書(shū)的Section 1.5,實(shí)際上本文也就是基于它的一篇讀后感吧。

原文中更多的是給出一些結(jié)論,我嘗試給出一些思路上的過(guò)程,即為什么要使用這個(gè)方法,而不是別的什么方法。我覺(jué)得這個(gè)可能更加有意義一些,相比于記下一些結(jié)論。


?

關(guān)于動(dòng)態(tài)連通性

我們看一張圖來(lái)了解一下什么是動(dòng)態(tài)連通性:

?

假設(shè)我們輸入了一組整數(shù)對(duì),即上圖中的(4, 3) (3, 8)等等,每對(duì)整數(shù)代表這兩個(gè)points/sites是連通的。那么隨著數(shù)據(jù)的不斷輸入,整個(gè)圖的連通性也會(huì)發(fā)生變化,從上圖中可以很清晰的發(fā)現(xiàn)這一點(diǎn)。同時(shí),對(duì)于已經(jīng)處于連通狀態(tài)的points/sites,直接忽略,比如上圖中的(8, 9)


?

動(dòng)態(tài)連通性的應(yīng)用場(chǎng)景:

  • 網(wǎng)絡(luò)連接判斷:

如果每個(gè)pair中的兩個(gè)整數(shù)分別代表一個(gè)網(wǎng)絡(luò)節(jié)點(diǎn),那么該pair就是用來(lái)表示這兩個(gè)節(jié)點(diǎn)是需要連通的。那么為所有的pairs建立了動(dòng)態(tài)連通圖后,就能夠盡可能少的減少布線的需要,因?yàn)橐呀?jīng)連通的兩個(gè)節(jié)點(diǎn)會(huì)被直接忽略掉。

  • 變量名等同性(類似于指針的概念)

在程序中,可以聲明多個(gè)引用來(lái)指向同一對(duì)象,這個(gè)時(shí)候就可以通過(guò)為程序中聲明的引用和實(shí)際對(duì)象建立動(dòng)態(tài)連通圖來(lái)判斷哪些引用實(shí)際上是指向同一對(duì)象。

?

對(duì)問(wèn)題建模:

在對(duì)問(wèn)題進(jìn)行建模的時(shí)候,我們應(yīng)該盡量想清楚需要解決的問(wèn)題是什么。因?yàn)槟P椭羞x擇的數(shù)據(jù)結(jié)構(gòu)和算法顯然會(huì)根據(jù)問(wèn)題的不同而不同,就動(dòng)態(tài)連通性這個(gè)場(chǎng)景而言,我們需要解決的問(wèn)題可能是:

  • 給出兩個(gè)節(jié)點(diǎn),判斷它們是否連通,如果連通,不需要給出具體的路徑
  • 給出兩個(gè)節(jié)點(diǎn),判斷它們是否連通,如果連通,需要給出具體的路徑

?

就上面兩種問(wèn)題而言,雖然只有是否能夠給出具體路徑的區(qū)別,但是這個(gè)區(qū)別導(dǎo)致了選擇算法的不同,本文主要介紹的是第一種情況,即不需要給出具體路徑的Union-Find算法,而第二種情況可以使用基于DFS的算法。

?

建模思路:

最簡(jiǎn)單而直觀的假設(shè)是,對(duì)于連通的所有節(jié)點(diǎn),我們可以認(rèn)為它們屬于一個(gè)組,因此不連通的節(jié)點(diǎn)必然就屬于不同的組。隨著Pair的輸入,我們需要首先判斷輸入的兩個(gè)節(jié)點(diǎn)是否連通。如何判斷呢?按照上面的假設(shè),我們可以通過(guò)判斷它們屬于的組,然后看看這兩個(gè)組是否相同,如果相同,那么這兩個(gè)節(jié)點(diǎn)連通,反之不連通。為簡(jiǎn)單起見(jiàn),我們將所有的節(jié)點(diǎn)以整數(shù)表示,即對(duì)N個(gè)節(jié)點(diǎn)使用0N-1的整數(shù)表示。而在處理輸入的Pair之前,每個(gè)節(jié)點(diǎn)必然都是孤立的,即他們分屬于不同的組,可以使用數(shù)組來(lái)表示這一層關(guān)系,數(shù)組的index是節(jié)點(diǎn)的整數(shù)表示,而相應(yīng)的值就是該節(jié)點(diǎn)的組號(hào)了。該數(shù)組可以初始化為:

[java] view plaincopy print?
  • for(int?i?=?0;?i?<?size;?i++)??
  • ????id[i]?=?i;????

  • 即對(duì)于節(jié)點(diǎn)i,它的組號(hào)也是i

    ?

    初始化完畢之后,對(duì)該動(dòng)態(tài)連通圖有幾種可能的操作:

    • 查詢節(jié)點(diǎn)屬于的組

    數(shù)組對(duì)應(yīng)位置的值即為組號(hào)

    • 判斷兩個(gè)節(jié)點(diǎn)是否屬于同一個(gè)組

    分別得到兩個(gè)節(jié)點(diǎn)的組號(hào),然后判斷組號(hào)是否相等

    • 連接兩個(gè)節(jié)點(diǎn),使之屬于同一個(gè)組

    分別得到兩個(gè)節(jié)點(diǎn)的組號(hào),組號(hào)相同時(shí)操作結(jié)束,不同時(shí),將其中的一個(gè)節(jié)點(diǎn)的組號(hào)換成另一個(gè)節(jié)點(diǎn)的組號(hào)

    • 獲取組的數(shù)目

    初始化為節(jié)點(diǎn)的數(shù)目,然后每次成功連接兩個(gè)節(jié)點(diǎn)之后,遞減1


    API

    我們可以設(shè)計(jì)相應(yīng)的API



    ?

    注意其中使用整數(shù)來(lái)表示節(jié)點(diǎn),如果需要使用其他的數(shù)據(jù)類型表示節(jié)點(diǎn),比如使用字符串,那么可以用哈希表來(lái)進(jìn)行映射,即將String映射成這里需要的Integer類型。

    ?

    分析以上的API,方法connectedunion都依賴于findconnected對(duì)兩個(gè)參數(shù)調(diào)用兩次find方法,而union在真正執(zhí)行union之前也需要判斷是否連通,這又是兩次調(diào)用find方法。因此我們需要把find方法的實(shí)現(xiàn)設(shè)計(jì)的盡可能的高效。所以就有了下面的Quick-Find實(shí)現(xiàn)。


    ?

    Quick-Find?算法:

    [java] view plaincopy print?
  • public?class?UF??
  • {??
  • ????private?int[]?id;?//?access?to?component?id?(site?indexed)??
  • ????private?int?count;?//?number?of?components??
  • ????public?UF(int?N)??
  • ????{??
  • ????????//?Initialize?component?id?array.??
  • ????????count?=?N;??
  • ????????id?=?new?int[N];??
  • ????????for?(int?i?=?0;?i?<?N;?i++)??
  • ????????????id[i]?=?i;??
  • ????}??
  • ????public?int?count()??
  • ????{?return?count;?}??
  • ????public?boolean?connected(int?p,?int?q)??
  • ????{?return?find(p)?==?find(q);?}??
  • ????public?int?find(int?p)??
  • ????{?return?id[p];?}??
  • ????public?void?union(int?p,?int?q)??
  • ????{???
  • ????????//?獲得p和q的組號(hào)??
  • ????????int?pID?=?find(p);??
  • ????????int?qID?=?find(q);??
  • ????????//?如果兩個(gè)組號(hào)相等,直接返回??
  • ????????if?(pID?==?qID)?return;??
  • ????????//?遍歷一次,改變組號(hào)使他們屬于一個(gè)組??
  • ????????for?(int?i?=?0;?i?<?id.length;?i++)??
  • ????????????if?(id[i]?==?pID)?id[i]?=?qID;??
  • ????????count--;??
  • ????}??
  • }??
  • 舉個(gè)例子,比如輸入的Pair(5?9),那么首先通過(guò)find方法發(fā)現(xiàn)它們的組號(hào)并不相同,然后在union的時(shí)候通過(guò)一次遍歷,將組號(hào)1都改成8。當(dāng)然,由8改成1也是可以的,保證操作時(shí)都使用一種規(guī)則就行。



    ?

    上述代碼的find方法十分高效,因?yàn)閮H僅需要一次數(shù)組讀取操作就能夠找到該節(jié)點(diǎn)的組號(hào),但是問(wèn)題隨之而來(lái),對(duì)于需要添加新路徑的情況,就涉及到對(duì)于組號(hào)的修改,因?yàn)椴⒉荒艽_定哪些節(jié)點(diǎn)的組號(hào)需要被修改,因此就必須對(duì)整個(gè)數(shù)組進(jìn)行遍歷,找到需要修改的節(jié)點(diǎn),逐一修改,這一下每次添加新路徑帶來(lái)的復(fù)雜度就是線性關(guān)系了,如果要添加的新路徑的數(shù)量是M,節(jié)點(diǎn)數(shù)量是N,那么最后的時(shí)間復(fù)雜度就是MN,顯然是一個(gè)平方階的復(fù)雜度,對(duì)于大規(guī)模的數(shù)據(jù)而言,平方階的算法是存在問(wèn)題的,這種情況下,每次添加新路徑就是“牽一發(fā)而動(dòng)全身”,想要解決這個(gè)問(wèn)題,關(guān)鍵就是要提高union方法的效率,讓它不再需要遍歷整個(gè)數(shù)組。

    ?

    Quick-Union?算法:

    考慮一下,為什么以上的解法會(huì)造成“牽一發(fā)而動(dòng)全身”?因?yàn)槊總€(gè)節(jié)點(diǎn)所屬的組號(hào)都是單獨(dú)記錄,各自為政的,沒(méi)有將它們以更好的方式組織起來(lái),當(dāng)涉及到修改的時(shí)候,除了逐一通知、修改,別無(wú)他法。所以現(xiàn)在的問(wèn)題就變成了,如何將節(jié)點(diǎn)以更好的方式組織起來(lái),組織的方式有很多種,但是最直觀的還是將組號(hào)相同的節(jié)點(diǎn)組織在一起,想想所學(xué)的數(shù)據(jù)結(jié)構(gòu),什么樣子的數(shù)據(jù)結(jié)構(gòu)能夠?qū)⒁恍┕?jié)點(diǎn)給組織起來(lái)?常見(jiàn)的就是鏈表,圖,樹(shù),什么的了。但是哪種結(jié)構(gòu)對(duì)于查找和修改的效率最高?毫無(wú)疑問(wèn)是樹(shù),因此考慮如何將節(jié)點(diǎn)和組的關(guān)系以樹(shù)的形式表現(xiàn)出來(lái)。

    ?

    如果不改變底層數(shù)據(jù)結(jié)構(gòu),即不改變使用數(shù)組的表示方法的話。可以采用parent-link的方式將節(jié)點(diǎn)組織起來(lái),舉例而言,id[p]的值就是p節(jié)點(diǎn)的父節(jié)點(diǎn)的序號(hào),如果p是樹(shù)根的話,id[p]的值就是p,因此最后經(jīng)過(guò)若干次查找,一個(gè)節(jié)點(diǎn)總是能夠找到它的根節(jié)點(diǎn),即滿足id[root] = root的節(jié)點(diǎn)也就是組的根節(jié)點(diǎn)了,然后就可以使用根節(jié)點(diǎn)的序號(hào)來(lái)表示組號(hào)。所以在處理一個(gè)pair的時(shí)候,將首先找到pair中每一個(gè)節(jié)點(diǎn)的組號(hào)(即它們所在樹(shù)的根節(jié)點(diǎn)的序號(hào)),如果屬于不同的組的話,就將其中一個(gè)根節(jié)點(diǎn)的父節(jié)點(diǎn)設(shè)置為另外一個(gè)根節(jié)點(diǎn),相當(dāng)于將一顆獨(dú)立的樹(shù)編程另一顆獨(dú)立的樹(shù)的子樹(shù)。直觀的過(guò)程如下圖所示。但是這個(gè)時(shí)候又引入了問(wèn)題。



    ?

    在實(shí)現(xiàn)上,和之前的Quick-Find只有findunion兩個(gè)方法有所不同:

    [java] view plaincopy print?
  • private?int?find(int?p)??
  • {???
  • ????//?尋找p節(jié)點(diǎn)所在組的根節(jié)點(diǎn),根節(jié)點(diǎn)具有性質(zhì)id[root]?=?root??
  • ????while?(p?!=?id[p])?p?=?id[p];??
  • ????return?p;??
  • }??
  • public?void?union(int?p,?int?q)??
  • {???
  • ????//?Give?p?and?q?the?same?root.??
  • ????int?pRoot?=?find(p);??
  • ????int?qRoot?=?find(q);??
  • ????if?(pRoot?==?qRoot)???
  • ????????return;??
  • ????id[pRoot]?=?qRoot;????//?將一顆樹(shù)(即一個(gè)組)變成另外一課樹(shù)(即一個(gè)組)的子樹(shù)??
  • ????count--;??
  • }??
  • ?

    樹(shù)這種數(shù)據(jù)結(jié)構(gòu)容易出現(xiàn)極端情況,因?yàn)樵诮?shù)的過(guò)程中,樹(shù)的最終形態(tài)嚴(yán)重依賴于輸入數(shù)據(jù)本身的性質(zhì),比如數(shù)據(jù)是否排序,是否隨機(jī)分布等等。比如在輸入數(shù)據(jù)是有序的情況下,構(gòu)造的BST會(huì)退化成一個(gè)鏈表。在我們這個(gè)問(wèn)題中,也是會(huì)出現(xiàn)的極端情況的,如下圖所示。



    ?

    為了克服這個(gè)問(wèn)題,BST可以演變成為紅黑樹(shù)或者AVL樹(shù)等等。

    ?

    然而,在我們考慮的這個(gè)應(yīng)用場(chǎng)景中,每對(duì)節(jié)點(diǎn)之間是不具備可比性的。因此需要想其它的辦法。在沒(méi)有什么思路的時(shí)候,多看看相應(yīng)的代碼可能會(huì)有一些啟發(fā),考慮一下Quick-Union算法中的union方法實(shí)現(xiàn):

    [java] view plaincopy print?
  • public?void?union(int?p,?int?q)??
  • {???
  • ????//?Give?p?and?q?the?same?root.??
  • ????int?pRoot?=?find(p);??
  • ????int?qRoot?=?find(q);??
  • ????if?(pRoot?==?qRoot)???
  • ????????return;??
  • ????id[pRoot]?=?qRoot;??//?將一顆樹(shù)(即一個(gè)組)變成另外一課樹(shù)(即一個(gè)組)的子樹(shù)??
  • ????count--;??
  • }??

  • 上面 id[pRoot] = qRoot 這行代碼看上去似乎不太對(duì)勁。因?yàn)檫@也屬于一種“硬編碼”,這樣實(shí)現(xiàn)是基于一個(gè)約定,即p所在的樹(shù)總是會(huì)被作為q所在樹(shù)的子樹(shù),從而實(shí)現(xiàn)兩顆獨(dú)立的樹(shù)的融合。那么這樣的約定是不是總是合理的呢?顯然不是,比如p所在的樹(shù)的規(guī)模比q所在的樹(shù)的規(guī)模大的多時(shí),pq結(jié)合之后形成的樹(shù)就是十分不和諧的一頭輕一頭重的”畸形樹(shù)“了。


    ?

    所以我們應(yīng)該考慮樹(shù)的大小,然后再來(lái)決定到底是調(diào)用:

    id[pRoot] = qRoot?或者是?id[qRoot] = pRoot



    ?

    即總是size小的樹(shù)作為子樹(shù)和size大的樹(shù)進(jìn)行合并。這樣就能夠盡量的保持整棵樹(shù)的平衡。

    ?

    所以現(xiàn)在的問(wèn)題就變成了:樹(shù)的大小該如何確定?

    我們回到最初的情形,即每個(gè)節(jié)點(diǎn)最一開(kāi)始都是屬于一個(gè)獨(dú)立的組,通過(guò)下面的代碼進(jìn)行初始化:

    [java] view plaincopy print?
  • for?(int?i?=?0;?i?<?N;?i++)??
  • ????id[i]?=?i;????//?每個(gè)節(jié)點(diǎn)的組號(hào)就是該節(jié)點(diǎn)的序號(hào)??


  • ?

    以此類推,在初始情況下,每個(gè)組的大小都是1,因?yàn)橹缓幸粋€(gè)節(jié)點(diǎn),所以我們可以使用額外的一個(gè)數(shù)組來(lái)維護(hù)每個(gè)組的大小,對(duì)該數(shù)組的初始化也很直觀:

    [java] view plaincopy print?
  • for?(int?i?=?0;?i?<?N;?i++)??
  • ????sz[i]?=?1;????//?初始情況下,每個(gè)組的大小都是1??


  • 而在進(jìn)行合并的時(shí)候,會(huì)首先判斷待合并的兩棵樹(shù)的大小,然后按照上面圖中的思想進(jìn)行合并,實(shí)現(xiàn)代碼:

    ?

    [java] view plaincopy print?
  • public?void?union(int?p,?int?q)??
  • {??
  • ????int?i?=?find(p);??
  • ????int?j?=?find(q);??
  • ????if?(i?==?j)?return;??
  • ????//?將小樹(shù)作為大樹(shù)的子樹(shù)??
  • ????if?(sz[i]?<?sz[j])?{?id[i]?=?j;?sz[j]?+=?sz[i];?}??
  • ????else?{?id[j]?=?i;?sz[i]?+=?sz[j];?}??
  • ????count--;??
  • }??

  • Quick-Union??Weighted Quick-Union?的比較:



    ?

    可以發(fā)現(xiàn),通過(guò)sz數(shù)組決定如何對(duì)兩棵樹(shù)進(jìn)行合并之后,最后得到的樹(shù)的高度大幅度減小了。這是十分有意義的,因?yàn)樵?span style="font-family:Calibri;" lang="en-us">Quick-Union算法中的任何操作,都不可避免的需要調(diào)用find方法,而該方法的執(zhí)行效率依賴于樹(shù)的高度。樹(shù)的高度減小了,find方法的效率就增加了,從而也就增加了整個(gè)Quick-Union算法的效率。

    ?

    上圖其實(shí)還可以給我們一些啟示,即對(duì)于Quick-Union算法而言,節(jié)點(diǎn)組織的理想情況應(yīng)該是一顆十分扁平的樹(shù),所有的孩子節(jié)點(diǎn)應(yīng)該都在height1的地方,即所有的孩子都直接連接到根節(jié)點(diǎn)。這樣的組織結(jié)構(gòu)能夠保證find操作的最高效率。

    ?

    那么如何構(gòu)造這種理想結(jié)構(gòu)呢?

    find方法的執(zhí)行過(guò)程中,不是需要進(jìn)行一個(gè)while循環(huán)找到根節(jié)點(diǎn)嘛?如果保存所有路過(guò)的中間節(jié)點(diǎn)到一個(gè)數(shù)組中,然后在while循環(huán)結(jié)束之后,將這些中間節(jié)點(diǎn)的父節(jié)點(diǎn)指向根節(jié)點(diǎn),不就行了么?但是這個(gè)方法也有問(wèn)題,因?yàn)?span style="font-family:Calibri;" lang="en-us">find操作的頻繁性,會(huì)造成頻繁生成中間節(jié)點(diǎn)數(shù)組,相應(yīng)的分配銷(xiāo)毀的時(shí)間自然就上升了。那么有沒(méi)有更好的方法呢?還是有的,即將節(jié)點(diǎn)的父節(jié)點(diǎn)指向該節(jié)點(diǎn)的爺爺節(jié)點(diǎn),這一點(diǎn)很巧妙,十分方便且有效,相當(dāng)于在尋找根節(jié)點(diǎn)的同時(shí),對(duì)路徑進(jìn)行了壓縮,使整個(gè)樹(shù)結(jié)構(gòu)扁平化。相應(yīng)的實(shí)現(xiàn)如下,實(shí)際上只需要添加一行代碼:

    [java] view plaincopy print?
  • private?int?find(int?p)??
  • {??
  • ????while?(p?!=?id[p])??
  • ????{??
  • ????????//?將p節(jié)點(diǎn)的父節(jié)點(diǎn)設(shè)置為它的爺爺節(jié)點(diǎn)??
  • ????????id[p]?=?id[id[p]];??
  • ????????p?=?id[p];??
  • ????}??
  • ????return?p;??
  • }??

  • 至此,動(dòng)態(tài)連通性相關(guān)的Union-Find算法基本上就介紹完了,從容易想到的Quick-Find到相對(duì)復(fù)雜但是更加高效的Quick-Union,然后到對(duì)Quick-Union的幾項(xiàng)改進(jìn),讓我們的算法的效率不斷的提高。

    這幾種算法的時(shí)間復(fù)雜度如下所示:

    Algorithm

    Constructor

    Union

    Find

    Quick-Find

    N

    N

    1

    Quick-Union

    N

    Tree height

    Tree height

    Weighted Quick-Union

    N

    lgN

    lgN

    Weighted Quick-Union With Path Compression

    N

    Very near to 1 (amortized)

    Very near to 1 (amortized)

    ?

    對(duì)大規(guī)模數(shù)據(jù)進(jìn)行處理,使用平方階的算法是不合適的,比如簡(jiǎn)單直觀的Quick-Find算法,通過(guò)發(fā)現(xiàn)問(wèn)題的更多特點(diǎn),找到合適的數(shù)據(jù)結(jié)構(gòu),然后有針對(duì)性的進(jìn)行改進(jìn),得到了Quick-Union算法及其多種改進(jìn)算法,最終使得算法的復(fù)雜度降低到了近乎線性復(fù)雜度。

    ?

    如果需要的功能不僅僅是檢測(cè)兩個(gè)節(jié)點(diǎn)是否連通,還需要在連通時(shí)得到具體的路徑,那么就需要用到別的算法了,比如DFS或者BFS。

    總結(jié)

    以上是生活随笔為你收集整理的并查集(Union-Find)算法介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。