圖的生成樹(森林)(克魯斯卡爾Kruskal算法和普里姆Prim算法)、以及並查集的使用


圖的連通性問題:無向圖的連通分量和生成樹,所有頂點均由邊連接在一起,但不存在回路的圖。

設圖 G=(V, E) 是個連通圖,當從圖任一頂點出發遍歷圖G 時,將邊集 E(G) 分成兩個集合 T(G) 和 B(G)。其中 T(G)是遍歷圖時所經過的邊的集合,B(G) 是遍歷圖時未經過的邊的集合。顯然,G1(V, T) 是圖 G 的極小連通子圖,即子圖G1 是連通圖 G 的生成樹。

深度優先生成森林

  右邊的是深度優先生成森林:

連通圖的生成樹不一定是唯一的不同的遍歷圖的方法得到不同的生成樹;從不同的頂點出發可得到不同的生成樹。
連通圖本身就是連通分量,其中頂點集+遍歷經過的邊=生成樹。
非連通圖的生成森林不一定是唯一的。
非連通圖各個連通分量的頂點集+遍歷時經過的邊=若干顆生成樹(生成森林)

最小生成樹 
給定一個無向網絡,在該網的所有生成樹中,使得各邊權數之和最小的那棵生成樹稱為該網的最小生成樹。

問題的提出:要在 n 個城市間建立交通網,要考慮的問題如何在保證 n 點連通的前題下最節省經費? 

如何求連通圖的最小生成樹?

構造最小生成樹的算法很多,其中多數算法都利用了一種稱之為 MST 的性質。

MST 性質:設 N = (V, E)  是一個連通網,U 是頂點集 V 的一個非空子集。若邊 (u, v) 是一條具有最小權值的邊,其中u∈U,v∈V-U,則必存在一棵包含邊 (u, v) 的最小生成樹。

方法一:普里姆 (Prim) 算法。

算法思想:

1、設 N=(V, E) 是連通網,TE 是N 上最小生成樹中邊的集合。初始令 U={u0}, (u0屬於V ), TE={ }。
2、在所有 u屬於U, v屬於V-U 的邊 (u, v)屬於E 中,
找一條代價最小的邊 (u0, v0)。
將 (u0, v0) 並入集合 TE,同時 v0 並入 U。
 
3、
重復上述操作直至 U=V 為止,則 T=(V, TE) 為 N 的最

小生成樹。

 

總得來說,普里姆算法就是以樹為單位,找最小的權邊,特點是針對無向圖!只和頂點有關,和邊無關,適用於稠密圖。算法時間復雜度為 O(n^2)

如圖:普里姆算法求最小生成樹

初始令 U={u0}, (u0屬於V ), TE={ }。

   

在所有 u屬於U, v屬於V-U 的邊 (u, v)屬於E 中,找一條代價最小的邊 (u0, v0)。將 (u0, v0) 並入集合 TE,同時 v0 並入 U。

   

重復上述操作直至 U=V 為止,則 T=(V, TE) 為 N 的最 小生成樹。

    

繼續

    

最后,遍歷完

    

Prim算法的實現  

頂點集合如何表示?最小邊如何選擇?一個頂點加入U集合如何表示?如下面的例子:
當U集合中加入一個新頂點時,V-U集合中的頂點到U的最小代價邊可能會更新,k 代表最終選擇的頂點,k=3,代表選擇是v3這個頂點,因為1-3代價是最小的=1
選取了 v3,之后,繼續以最新的樹為單位,來找最小的權值邊,通過看和哪個頂點連接。
k=6,代表選擇是v6這個頂點,因為3-6代價是最小的=4,在所有的和最新的樹鄰接的頂點中,權值最小的邊。
選取 v6之后
繼續以最新的樹為單位,找臨近的頂點,看哪條邊的權值最小,找到6-4這條邊,權值=2
新的樹如圖
繼續以最新的樹為單位,找臨近的頂點,看哪條邊的權值最小,找到3-2這條邊,權值=5
新的樹如圖
繼續以最新的樹為單位,找臨近的頂點,看哪條邊的權值最小,找到2-5這條邊,權值=3
直到所有頂點全部並入生成樹之后,程序結束
 

方法二:克魯斯卡爾 (Kruskal) 算法。

使用了並查集,直接從邊中找到不成環的最小的權邊(最簡單的求最小生成樹的算法),特點:只針對無向圖,包好普里姆算法,都是只針對無向圖。

算法思想:

1、設連通網  N = (V, E ),令最小生成樹初始狀態為只有 n 個頂點而無邊的非連通圖 T=(V, { }),每個頂點自成一個連通分量。
2、 在 E 中選取代價最小的邊,若該邊依附 的頂點落在 T 中不同的連通分量上(即: 不能形成環),則將此邊加入到 T 中;否 則,舍去此邊,選取下一條代價最小的邊。
3、依此類推,直至 T 中所有頂點都在同一連通分量上為止。

最小生成樹可能不惟一(包括普里姆算法都是一樣的道理)

把所有的邊按照權值升序排列,從最小邊開始(不能形成回路),選取,組成最小生成樹。直到所有的邊並入則結束(不是頂點!) 克魯斯卡爾算法主要在排序邊的權值序列的時候最費時間,他的算法時間復雜度和排序算法有關,而排序算法的時間復雜度和圖的邊 e 有關系,和頂點 v 沒有關系。故適用於稀疏圖。(而普里姆算法適合稠密圖
下面是圖解步驟:
按照升序,找出權值的排序序列:1 2 3 4 5 5 5 6 6 6
注意選取權值最小的邊的時候,不要形成回路
按照權值的升序排列的順序查找選取合適的邊
繼續,按照權值的升序排列的順序查找選取合適的邊
注意選取5的時候,避免環的生成,即可
直到所有的邊都並入即可。
那么在克魯斯卡爾算法里,通過找合適的邊,該如何避免形成回路呢?換句話說,如何判斷是否形成了回路?

使用並查集可以判斷是否形成了回路,kruskal算法用到了一種貪心策略,首先要把邊集數組以邊的權值從小到大排序,然后一條邊一條邊的查找,如果邊的兩個端點不在一個集合內,則將此邊添加到正在生長的樹林中,並合並兩個端點所在的集合,直到最小生成樹已生成完畢。

並查集:
是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合並及查詢問題。常常在使用中以森林來表示。集就是讓每個元素構成一個單元素的集合,也就是按一定順序將屬於同一組的元素所在的集合合並。

並查集是一種非常簡單的數據結構,它主要涉及兩個基本操作,分別為:

A. 合並兩個不相交集合

B. 判斷兩個元素是否屬於同一個集合

1)合並兩個不相交集合(Union(x,y))

合並操作很簡單:先設置一個數組Father[x],在克魯斯卡爾算法里,需要使用雙親存儲結構,表示x的“父親”的編號。那么,合並兩個不相交集合的方法就是,找到其中一個集合最父親的父親(也就是最久遠的祖先),將另外一個集合的最久遠的祖先的父親指向它。

通俗的說,就是把其中一個樹的根,作為另一個樹的根結點的一個孩子結點即可。

上圖為兩個不相交集合,合並后可以看出:Father(b)=Father(g)=f 結點

2)判斷兩個元素是否屬於同一集合(Find_Set(x)),本操作可轉換為尋找兩個元素的最久遠祖先是否相同。可以采用遞歸實現。

並查集的優化問題

尋找祖先時,我們一般采用遞歸查找,但是當元素很多亦或是整棵樹變為一條鏈時,每次Find_Set(x)都是O(n)的復雜度。為了避免這種情況,我們需對路徑進行壓縮,即當我們經過”遞推”找到祖先節點后,”回溯”的時候順便將它的子孫節點都直接指向祖先,這樣以后再次Find_Set(x)時復雜度就變成O(1)了,如下圖所示。可見,路徑壓縮方便了以后的查找。

回到克魯斯卡爾算法,使用並查集來實現判斷回路的生成否

比如從 v1開始(一共是 v1、v2、v3、v4、v5、v6),則開始把 v1-v6作為各個單根樹,以森林來表示,讓每個元素構成一個個的單元素的集合,需要使用數組表示,存儲方式就是雙親存儲結構(方便找到共同的父親)。

每次使用並查集,將后入的邊上的另一個結點作為孩子結點,而沒有加入的結點還是去做為單根的樹:

如圖所示,上圖,該選取權值=5的邊了,此時有兩個樹

   和   

如果選取3-4或者1-4這兩條邊的任意一個,單根樹是不會產生根相同的情形的,而加入的(作為孩子的根),一定會找到共同祖先的,這樣就可以發現回路的存在! 而選取2-3這條邊的話,在並查集中,就不會查出共同的祖先,也就是沒有環的形成。

通俗的說,就是通過兩個元素所在的結點推出跟結點,若根相同,則為同一個集合,否則不是同一個集合(也就是不形成回路)

 

歡迎關注

 

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM