1. 程式人生 > >圖論4之圖的最小生成樹及拓撲排序

圖論4之圖的最小生成樹及拓撲排序

生成樹

 同一個連通圖可以有不同的生成樹。例如對於圖9-1(a),其餘3個子圖都是它的生成樹。在每棵生成樹中都包含8個頂點和7條邊,即n個頂點和n-1條邊,此時n等於原圖中的頂點數8,它們的差別只是邊的選取方法不同。

       在這3棵生成樹中,圖9-1(b)中的邊集是從圖9-1(a)中的頂點V0出發,利用深度優先搜尋遍歷的方法而得到的邊集,此圖是原圖的深度優先生成樹;圖9-1(c)中的邊集是從圖9-1(a)中的頂點V0出發,利用廣度優先搜尋遍歷的方法而得到的邊集,此圖是原圖的廣度優先生成樹;圖9-1(d)是原圖的任意一棵生成樹。當然圖9-1(a)的生成樹遠不止這3種,只要能連通所有頂點而又不產生迴路的任何子圖都是它的生成樹。

生成樹: 如果連通圖G的一個子圖是一棵包含G的所有頂點的樹,則該子圖稱為G的生成樹。  生成樹是連通圖的包含圖中的所有頂點的極小連通子圖。它並不唯一,從不同的頂點出發進行遍歷,可以得到不同的生成樹。  

其中,權值最小的樹就是最小生成樹。

關於最小生成樹最經典的應用模型就是城市通訊線路網最小造價的問題:網路G表示n個城市之間的通訊線路(其中頂點表示城市,邊表示兩個城市之間的通訊線路,邊上的權值表示線路的長度或造價),通過求該網路的最小生成樹找到求解通訊線路總造價最小的最佳方案。

求圖的最小生成樹主要有兩種經典演算法:

1,普里姆(Prim)演算法 時間複雜度為O(n2),適合於求邊稠密的最小生成樹。 2,克魯斯卡爾(Kruskal)演算法

一、Prim演算法

1.概覽

普里姆演算法(Prim演算法),圖論中的一種演算法,可在加權連通圖裡搜尋最小生成樹。意即由此演算法搜尋到的邊子集所構成的樹中,不但包括了連通圖裡的所有頂點(英語:Vertex (graph theory)),且其所有邊的權值之和亦為最小。該演算法於1930年由捷克數學家沃伊捷赫·亞爾尼克(英語:Vojtěch Jarník)發現;並在1957年由美國電腦科學家羅伯特·普里姆(英語:Robert C. Prim)獨立發現;1959年,艾茲格·迪科斯徹再次發現了該演算法。因此,在某些場合,普里姆演算法又被稱為DJP演算法、亞爾尼克演算法或普里姆-亞爾尼克演算法。

2.演算法簡單描述

取圖中任意一個頂點V作為生成樹的根,之後若要往生成樹上新增頂點W,則在頂點V和W之間必定存在一條邊。並且該邊的權值在所有連通頂點V和W之間的邊中取值最小。

下面對演算法的圖例描述

圖例 說明 不可選 可選 已選(Vnew)
 

此為原始的加權連通圖。每條邊一側的數字代表其權值。 - - -

頂點D被任意選為起始點。頂點ABEF通過單條邊與D相連。A是距離D最近的頂點,因此將A及對應邊AD以高亮表示。 C, G A, B, E, F D
 

下一個頂點為距離DA最近的頂點。BD為9,距A為7,E為15,F為6。因此,FDA最近,因此將頂點F與相應邊DF以高亮表示。 C, G B, E, F A, D
演算法繼續重複上面的步驟。距離A為7的頂點B被高亮表示。 C B, E, G A, D, F
 

在當前情況下,可以在CEG間進行選擇。CB為8,EB為7,GF為11。E最近,因此將頂點E與相應邊BE高亮表示。 C, E, G A, D, F, B
 

這裡,可供選擇的頂點只有CGCE為5,GE為9,故選取C,並與邊EC一同高亮表示。 C, G A, D, F, B, E

頂點G是唯一剩下的頂點,它距F為11,距E為9,E最近,故高亮表示G及相應邊EG G A, D, F, B, E, C

現在,所有頂點均已被選取,圖中綠色部分即為連通圖的最小生成樹。在此例中,最小生成樹的權值之和為39。 A, D, F, B, E, C, G

3.簡單證明prim演算法

反證法:假設prim生成的不是最小生成樹

1).設prim生成的樹為G0

2).假設存在Gmin使得cost(Gmin)<cost(G0)   則在Gmin中存在<u,v>不屬於G0

3).將<u,v>加入G0中可得一個環,且<u,v>不是該環的最長邊(這是因為<u,v>∈Gmin)

4).這與prim每次生成最短邊矛盾

5).故假設不成立,命題得證.

4.時間複雜度

這裡記頂點數v,邊數e

鄰接矩陣:O(v2)                 鄰接表:O(elog2v)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Prim演算法
"""
def prim(graph, vertex_num):
    INF = 1 << 10
    visit = [False] * vertex_num
    dist = [INF] * vertex_num
    #preIndex = [0] * vertex_num
    #對所有的頂點進行迴圈,首先是確定頭結點
    #找到當前無向圖的最小生成樹
    for i in range(vertex_num):
        minDist = INF + 1
        nextIndex = -1
        #第一次迴圈時,nextIndex就是頭結點
        #所以要把minDIst加上1,之後這個迴圈
        #的功能是找到基於當前i,鄰接矩陣中i行到哪一行距離最小的那個位置作為下一個結點,當然前提是那個結點沒有去過
        for j in range(vertex_num):
            if dist[j] < minDist and not visit[j]:
                minDist = dist[j]
                nextIndex = j
        print (nextIndex)
        visit[nextIndex] = True
        #由於前面已經找到了下一個結點了,現在就要構建再下一個結點的dist矩陣了,這就要看當前這個nextIndex這一行了
        for j in range(vertex_num):
            if dist[j] > graph[nextIndex][j] and not visit[j]:
                dist[j] = graph[nextIndex][j]
                #preIndex[j] = nextIndex
    return dist, #preIndex

if __name__ == '__main__':
    _ = 1 << 10  # init inf
    graph = [
        [0, 6, 3, _, _, _],
        [6, 0, 2, 5, _, _],
        [3, 2, 0, 3, 4, _],
        [_, 5, 3, 0, 2, 3],
        [_, _, 4, 2, 0, 5],
        [_, _, _, 3, 5, 0],
    ]
    prim(graph, 6)

二、Kruskal演算法

1.概覽

Kruskal演算法是一種用來尋找最小生成樹的演算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim演算法和Boruvka演算法等。三種演算法都是貪婪演算法的應用。和Boruvka演算法不同的地方是,Kruskal演算法在圖中存在相同權值的邊時也有效。

2.演算法簡單描述

1).記Graph中有v個頂點,e個邊

2).新建圖Graphnew,Graphnew中擁有原圖中相同的e個頂點,但沒有邊

3).將原圖Graph中所有e個邊按權值從小到大排序

4).迴圈:從權值最小的邊開始遍歷每條邊 直至圖Graph中所有的節點都在同一個連通分量中

                if 這條邊連線的兩個節點於圖Graphnew中不在同一個連通分量中

                                         新增這條邊到圖Graphnew中

圖例描述:

首先第一步,我們有一張圖Graph,有若干點和邊 

將所有的邊的長度排序,用排序的結果作為我們選擇邊的依據。這裡再次體現了貪心演算法的思想。資源排序,對區域性最優的資源進行選擇,排序完成後,我們率先選擇了邊AD。這樣我們的圖就變成了右圖

在剩下的變中尋找。我們找到了CE。這裡邊的權重也是5

依次類推我們找到了6,7,7,即DF,AB,BE。

下面繼續選擇, BC或者EF儘管現在長度為8的邊是最小的未選擇的邊。但是現在他們已經連通了(對於BC可以通過CE,EB來連線,類似的EF可以通過EB,BA,AD,DF來接連)。所以不需要選擇他們。類似的BD也已經連通了(這裡上圖的連通線用紅色表示了)。

最後就剩下EG和FG了。當然我們選擇了EG。最後成功的圖就是右:

3.簡單證明Kruskal演算法

對圖的頂點數n做歸納,證明Kruskal演算法對任意n階圖適用。

歸納基礎:

n=1,顯然能夠找到最小生成樹。

歸納過程:

假設Kruskal演算法對n≤k階圖適用,那麼,在k+1階圖G中,我們把最短邊的兩個端點a和b做一個合併操作,即把u與v合為一個點v',把原來接在u和v的邊都接到v'上去,這樣就能夠得到一個k階圖G'(u,v的合併是k+1少一條邊),G'最小生成樹T'可以用Kruskal演算法得到。

我們證明T'+{<u,v>}是G的最小生成樹。

用反證法,如果T'+{<u,v>}不是最小生成樹,最小生成樹是T,即W(T)<W(T'+{<u,v>})。顯然T應該包含<u,v>,否則,可以用<u,v>加入到T中,形成一個環,刪除環上原有的任意一條邊,形成一棵更小權值的生成樹。而T-{<u,v>},是G'的生成樹。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),產生了矛盾。於是假設不成立,T'+{<u,v>}是G的最小生成樹,Kruskal演算法對k+1階圖也適用。

由數學歸納法,Kruskal演算法得證。

時間複雜度:elog2e  e為圖中的邊數

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
__mtime__ = '2018/10/22'
"""
tinyEWG_txt = """
{
    0:[(6,0.58),(2,0.26),(4,0.38),(7,0.16)],
    1:[(3,0.29),(2,0.36),(7,0.19),(5,0.32)],
    2:[(6,0.40),(7,0.34),(1,0.36),(0,0.26),(3,0.17)],
    3:[(6,0.52),(1,0.29),(2,0.17)],
    4:[(6,0.93),(0,0.38),(7,0.37),(5,0.35)],
    5:[(1,0.32),(7,0.28),(4,0.35)],
    6:[(4,0.93),(0,0.58),(3,0.52),(2,0.40)],
    7:[(2,0.34),(1,0.19),(0,0.16),(5,0.28)]
}
"""
## 模擬優先佇列
class Min_PQ_:
    def __init__(self, f, reversed=False):
        self.data = []
        self.f = f
        self.__reversed = reversed

    def remove(self):
        return self.data.pop(0)

    def insert(self, item):
        self.data.append(item)
        self.data.sort(key=self.f, reverse=self.__reversed)
    def __len__(self):
        return len(self.data)
    def __str__(self):
        return str(self.data)

# 補全edge
def fill(g):
    for v in g:
        for i in range(0, len(g[v])):
            g[v][i] = (v,) + g[v][i]

def mst_prim(g):
    visited = set()
    pq = Min_PQ_(lambda x: x[-1])
    mst = []
    def visit(v):
        visited.add(v)
        for edge in g.get(v, []):
            pq.insert(edge)
    visit(0)
    while pq:
        min_edge = pq.remove()
        x, y = min_edge[0], min_edge[1]
        if x in visited and y in visited:
            continue
        else:
            mst.append(min_edge)
            if x not in visited: visit(x)
            if y not in visited: visit(y)
    return mst

def get_edges_set(g):
    s = set()
    for v in g:
        for e in g.get(v, []):
            a, b, weight = e[0], e[1], e[-1]
            if a < b:
                s.add((a, b, weight))
            else:
                s.add((b, a, weight))
    return s

def add_edge(g, x, y):
    if x in g:
        (g[x]).append(y)
    else:
        g[x] = []
        add_edge(g, x, y)

def has_connected(g, x, y):
    has_connected__ = False
    visited = set()
    def __dfs(__x):
        visited.add(__x)
        for v in g.get(__x, []):
            if v == y:
                nonlocal has_connected__
                has_connected__ = True
                return
            elif v not in visited:
                __dfs(v)
    __dfs(x)
    return has_connected__

def mst_kruskal(g):
    sorted_edges = list(get_edges_set(g))
    sorted_edges.sort(key=lambda x: x[-1])
    mst = []
    g_temp = dict()
    while sorted_edges:
        min_edge = sorted_edges.pop(0)
        x, y = min_edge[0], min_edge[1]
        if has_connected(g_temp, x, y):
            continue
        else:
            mst.append(min_edge)
            add_edge(g_temp, x, y)
            add_edge(g_temp, y, x)
    return mst

if __name__ == '__main__':
    g = eval(tinyEWG_txt)
    fill(g)
    print(mst_kruskal(g))

三、拓撲排序

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
拓撲排序
"""
def indegree0(v, e):
    if v == []:
        return None
    tmp = v[:]
    for i in e:
        if i[1] in tmp:
            tmp.remove(i[1])
    if tmp == []:
        return -1
    for t in tmp:
        for i in range(len(e)):
            if t in e[i]:
                e[i] = 'toDel'  # 佔位,之後刪掉
    if e:
        eset = set(e)
        eset.remove('toDel')
        e[:] = list(eset)
    if v:
        for t in tmp:
            v.remove(t)
    return tmp

def topoSort(v, e):
    result = []
    while True:
        nodes = indegree0(v, e)
        if nodes == None:
            break
        if nodes == -1:
            print('there\'s a circle.')
            return None
        result.extend(nodes)
    return result

if __name__ == '__main__':
    v = ['a', 'b', 'c', 'd', 'e']
    e = [('a', 'b'), ('a', 'd'), ('b', 'c'), ('d', 'c'), ('d', 'e'), ('e', 'c')]
    res = topoSort(v, e)
    print(res)