算法導論(十六)--貪心算法,最小生成樹

算法導論(十六)--貪心算法,最小生成樹

圖的表示方式

先回顧一下圖(Graph)的基礎知識,有向圖(Digraph) G=(V, E),V是頂點集合,E是V*V條邊的子集。無向圖(Undirected graph)中E是無序的頂點對。
邊的大小|E|=O(V2),如果G是連通的(圖中任意頂點,都有路徑到達其它頂點),那麼|E|≥|V|-1 。

1.鄰接矩陣
G=(V, E),G的頂點集V={1,2,…,n},那麼G的鄰接矩陣是一個n*n的矩陣A,A[i,j]={1(i,j)E0A[i, j] = \begin{cases} 1 & 如果(i,j)∈E \\ 0 & 其他 \end{cases}
它也就是說,如果邊集裏存在一條邊eij,那麼Aij=1。有時候,圖的邊是加權的,那麼就用邊的權重來替換1 。
舉例:
在這裏插入圖片描述
上面這個有向圖的鄰接矩陣如下:

A 1 2 3 4
1 0 1 1 0
2 0 0 1 0
3 0 0 0 0
4 0 0 1 0

需要的存儲空間是O(V2),這就是稠密表示。如果圖是稠密的,即邊的數量接近最大可能邊數,效果會很好。但大多數圖都是稀疏的,比如鏈表,平面圖,樹,都是稀疏圖。稠密圖的一個例子是完全圖。

2.鄰接表
如果不想耗費這麼多空間來存一個稀疏圖,那麼鄰接表是一種不錯的方法。
一個給定頂點的鄰接表Adj(v),記錄了與V相鄰的頂點。上面的例子中,
Adj[1]={2,3}
Adj[2]={3}
Adj[3]={ }
Adj[4]={3}

分析一個這種方法的存儲空間:
對於一個頂點V,它的鄰接表的長度就是它的度。即Adj[V]={degree(v)outdegree(v)|Adj[V]|= \begin{cases} degree(v) & 無向圖中 \\ out-degree(v) &有向圖中 \end{cases}

一個重要的引理,叫做握手引理:
無向圖中,所有頂點的度加起來等於邊數的兩倍。

這種方法佔的存儲空間是2|E|+V(爲什麼要加V,想象一個只有頂點,沒有邊的圖,它仍然要佔用大小爲|V|的空間),因此是O(V+E)。對有向圖來說,要做的就是把所有的出度加起來,結果就是|E|。複雜度也是一樣。

鄰接法是一種稀疏表示法,一般都比鄰接矩陣要好。鄰接矩陣的一個優點是,每一條邊都可以用一個單獨的位來表示,而鄰接表需要O(logV)位來表示每個鄰接的邊,因爲需要logV個位來表示每個頂點。

最小生成樹

這個算法有海量的應用,在分佈式系統中非常重要(找出一棵最小生成樹,所有節點在任意時刻都是活躍的),它也是AT&T的記賬系統的基礎。

問題定義:輸入是一個連通的無向圖G=(V, E),和一個給邊加權的函數w(簡單起見,我們假設所有邊的權值都是互異的)。輸出是一棵最小生成樹,最小生成樹是指連接了所有頂點,並且權重之和最小的連通的無環圖。

舉例:
在這裏插入圖片描述
上面這個圖的MST:
在這裏插入圖片描述
從這幅圖裏,我們可以看出最優子結構的性質。
有一棵圖G的MST,記作T(圖裏的其它邊我們忽略不畫):
在這裏插入圖片描述
觀察方法是,任意的在MST中移除一條邊(u, v) ,T就被分成了兩棵子樹T1和T2:
在這裏插入圖片描述
我們現在要證明它闡述了最優子結構的特性:T1是圖G1的最小生成樹,T2是圖G2的最小生成樹。其中G1是一個由T1的頂點導出的圖G的子圖,也就是說V1是T1中的頂點集,E1是T1中頂點對的集,(x, y)是E1裏的邊(x和y都是V1裏的頂點)。同理T2也一樣。

證明:
用剪貼法。
T的權值w(T)=W(u,v)+w(T1)+w(T2)w(T)=W(u,v)+w(T1)+w(T2),假設存在T1’ ,它比T1有更低的權重,那我們就能得到一棵樹T’ ,它包含了邊(u, v)∪T1’∪T2,那就得到了一個新的生成樹,它比T的權重更小。

那麼對於這類問題,會不會有重疊子問題的特性呢?試想一下,我們移除一條邊,然後樹就分成了兩半,再選另一條邊移除,然後再選一條邊移除,會不會到最後有一堆相似的子問題?如果只是簡單的改改取出的順序,那麼會得到一堆相似的子問題。因此我們想到用動態規劃來解決。但最小生成樹還有一個隱藏的特點,一個局部最優解也是全局最優解,這就是貪心算法的標誌。

定理:設圖G=(V, E)的MST爲T,A是V的任意子集,假設邊(u, v)是連接A和V-A的具有最小權值的邊,那麼邊(u, v)屬於最小生成樹T。

看上面例子的圖。選一個單獨的頂點作爲A,比如最上面的頂點,那麼其它的所有頂點就是V-A,連接這兩個集合的邊中,那麼6這條邊肯定在T中。這個定理可以用剪貼法證明。

最小生成樹的算法:Prim算法

思路:把V-A維護成一個優先隊列Q,Q裏的每一個頂點,都有一個鍵,就是連接A和V-A之間的權值最小的邊的權值。

僞代碼

Q <- V //一開始把A設成空集
v.key <-for ∀ v ∈ V //鍵初始化爲無窮大
s.key <- 0 for s ∈ V //從V中任意選一頂點s

while(Q≠∅)
do u <- Extract_Min(Q) //每次從隊列裏取出最小的元素u
for each v ∈ Adj[u] //對於每一步,在鄰接表裏(也就是從v到u的邊)
	do if v ∈ Q and w(u,v)<v.key //如果v仍屬於集合V-A的話,就進行查看(所以取出來的元素就成了A的一部分)
		then v.key <- w(u,v) 
		T(v) <- u //每次加入A時都追蹤哪一個是相關的頂點
At end, {(v,π[v]} forms MST

舉例
在這裏插入圖片描述
頂點依次編好號。
一開始所有頂點的key都是無窮大;
接下來找一個頂點,s,比如是6號節點,把它加入集合A;
觀察s的鄰接表裏的每條邊,看是否在Q裏;
發現3,7,8都在Q裏,於是看它們的鍵是否小於邊的權值。因此把3的鍵設爲7,7的鍵設爲15,8的鍵設爲10;
回到隊列裏,找到鍵最小的頂點,是鍵值7對應的點3,取出,然後更新它的鄰接點的鍵5,12,9(7不用再更新,因爲它已經不是Q裏的元素);
回到隊列裏,選擇最小的,5對應的點2,更新它的鄰接點爲6,14,8;
回到隊列裏,選擇最小的,6對應的點1,已經沒有相關的頂點了,因爲頂點2,3都在A裏了。現在Q中最小的鍵就是8,於是更新頂點7的鄰接點爲3(覆蓋14);
回到隊列裏,選擇最小的,3對應的點5;
沒有什麼頂點好更新的了,最後加上9和15 。
最後結果就是邊7,5,6,8,3,9,15。

分析
初始化的複雜度是O(V),while循環的複雜度O(V·TExtract_Min+E·TDecrease_key),具體是多少取決於實現方式,如果用無序數組,就是=O(V·O(V)+E·O(1))=O(V2);如果是二叉堆,就是=O(V·O(logV)+E·O(logV))=O(E·logV);如果是斐波那契堆,就是=O(V·O(logV)+E·O(1))=O(V·logV+E)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章