算法快學筆記(十四):圖的最小生成樹算法

1. 介紹

最小生成樹的應用場景很廣,例如電信公司需要將9個村莊進行網絡連接,村莊間的距離都不相同,怎麼連接才能達到成本最小了?村莊結構圖如下:
在這裏插入圖片描述

V0-V10分別表示村莊,節點間的權重代表距離,連接所有節點的總距離最小,就可以讓成本更低。

定義:把構造連通整個圖的最小代價生成樹稱爲最小生成樹。

2. 相關算法

普利姆與克魯斯卡爾算法都是貪心算法

2.1 普利姆(Prim)算法

2.1.1 原理

普里姆算法(Prim算法):該算法於1930年由捷克數學家沃伊捷赫·亞爾尼克(英語:Vojtěch Jarník)發現;並在1957年由美國計算機科學家羅伯特·普里姆(英語:Robert C. Prim)獨立發現;1959年,艾茲格·迪科斯徹再次發現了該算法。因此,在某些場合,普里姆算法又被稱爲DJP算法、亞爾尼克算法或普里姆-亞爾尼克算法。

算法的基本步驟如下:

  1. 輸入:一個加權連通圖,其中頂點集合爲V,邊集合爲E;

  2. 初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;

  3. 重複下列操作,直到Vnew = V:

  • 在集合E中選取權值最小的邊<u, v>,其中u爲集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
  • 將v加入集合Vnew中,將<u, v>邊加入集合Enew中;
  1. 輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。

算法實例推理步驟如下:
在這裏插入圖片描述

2.1.2 代碼實現

基於鄰接矩陣存儲結構用Python實現的代碼如下:

# -*- coding:utf-8 -*-
# @Author:sunaihua
"""
普里姆算法(Prim算法):最小生成樹算法
本實現基於鄰接矩陣存儲結構
"""
MAX_VALUE = 65536

# 圖結構的矩陣存儲
graph = [
    [0, 10, MAX_VALUE, MAX_VALUE, MAX_VALUE, 11, MAX_VALUE, MAX_VALUE, MAX_VALUE],
    [10, 0, 18, MAX_VALUE, MAX_VALUE, MAX_VALUE, 16, MAX_VALUE, 12],
    [MAX_VALUE, MAX_VALUE, 0, 22, MAX_VALUE, MAX_VALUE, MAX_VALUE, MAX_VALUE, 8],
    [MAX_VALUE, MAX_VALUE, 22, 0, 20, MAX_VALUE, MAX_VALUE, 16, 21],
    [MAX_VALUE, MAX_VALUE, MAX_VALUE, 20, 0, 26, MAX_VALUE, 7, MAX_VALUE],
    [11, MAX_VALUE, MAX_VALUE, MAX_VALUE, 26, 0, 17, MAX_VALUE, MAX_VALUE],
    [MAX_VALUE, 16, MAX_VALUE, MAX_VALUE, MAX_VALUE, 17, 0, 19, MAX_VALUE],
    [MAX_VALUE, MAX_VALUE, MAX_VALUE, 16, 7, MAX_VALUE, 19, 0, MAX_VALUE],
    [MAX_VALUE, 12, 8, 21, MAX_VALUE, MAX_VALUE, MAX_VALUE, MAX_VALUE, 0],
]

start_node = 0  # 0節點爲初始節點

new_v_array = [] #最小生成樹的vertex數組
edge_value_array = [] #最小生成樹的權值數組
new_e_array = [] #最小生成樹的edge數組


def prim():
    v_number = len(graph)
    latest_edge_target = -1
    new_v_array.append(start_node)
    while True:
        if len(new_v_array) < v_number:
            smallest_value = MAX_VALUE
            for n_index in new_v_array:
                cur_net = graph[n_index]
                for index, value in enumerate(cur_net): # 選出new_v_array中所有邊的最小值
                    if index not in new_v_array:
                        if (value != 0) and (value <= smallest_value):
                            smallest_value = value
                            latest_edge_target = index
                            latest_edge_src = n_index
                    else:
                        continue
                    print(new_v_array, n_index, index, value, smallest_value, latest_edge_target)
            if latest_edge_target not in new_v_array:
                print("merge:", latest_edge_src, latest_edge_target, smallest_value)
                new_e_array.append([latest_edge_src, latest_edge_target])
                new_v_array.append(latest_edge_target)
                edge_value_array.append(smallest_value)
        else:
            break

    print("V seq:", new_v_array)
    print("E seq:", new_e_array)
    print("sum value:", sum(edge_value_array))


if __name__ == '__main__':
    prim()

2.1.3 結果

程序的部分輸出結果爲:

('V seq:', [0, 1, 5, 8, 2, 6, 7, 4, 3])
('E seq:', [[0, 1], [0, 5], [1, 8], [8, 2], [1, 6], [6, 7], [7, 4], [7, 3]])
('sum value:', 99)

連接文章開頭部分的最小值爲99.

關於時間複雜度問題:

如果記頂點數v,邊數e,則採用鄰接矩陣的算法複雜度爲:

O = V^2

採用鄰接表的時間複雜度爲:O(elog2v)

2.2 克魯斯卡爾(Kruskal)算法

2.2.1 原理

Kruskal算法是一種用來尋找最小生成樹的算法,由Joseph Kruskal在1956年發表。

算法簡單描述如下:

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

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

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

4).循環:從權值最小的邊開始遍歷每條邊 直至圖Graph中所有的節點都在同一個連通分量中,如果 這條邊連接的兩個節點於圖Graph_new中不在同一個連通分量中 則添加這條邊到圖Graph_new中

推理過程如下:
圖例描述:

首先第一步,我們有一張圖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。最後成功的圖如下所示:
在這裏插入圖片描述

2.1.2 實現

# -*- coding:utf-8 -*-
# @Author:sunaihua

"""
Kruskal:最小生成樹算法
本實現基於鄰接矩陣存儲結構
"""
import copy

MAX_VALUE = 65536

# 圖結構的矩陣存儲
graph = [
    [0, 10, MAX_VALUE, MAX_VALUE, MAX_VALUE, 11, MAX_VALUE, MAX_VALUE, MAX_VALUE],
    [10, 0, 18, MAX_VALUE, MAX_VALUE, MAX_VALUE, 16, MAX_VALUE, 12],
    [MAX_VALUE, MAX_VALUE, 0, 22, MAX_VALUE, MAX_VALUE, MAX_VALUE, MAX_VALUE, 8],
    [MAX_VALUE, MAX_VALUE, 22, 0, 20, MAX_VALUE, MAX_VALUE, 16, 21],
    [MAX_VALUE, MAX_VALUE, MAX_VALUE, 20, 0, 26, MAX_VALUE, 7, MAX_VALUE],
    [11, MAX_VALUE, MAX_VALUE, MAX_VALUE, 26, 0, 17, MAX_VALUE, MAX_VALUE],
    [MAX_VALUE, 16, MAX_VALUE, MAX_VALUE, MAX_VALUE, 17, 0, 19, MAX_VALUE],
    [MAX_VALUE, MAX_VALUE, MAX_VALUE, 16, 7, MAX_VALUE, 19, 0, MAX_VALUE],
    [MAX_VALUE, 12, 8, 21, MAX_VALUE, MAX_VALUE, MAX_VALUE, MAX_VALUE, 0],
]

new_v_array = []  # 最小生成樹的vertex數組
edge_value_array = []  # 最小生成樹的權值數組
new_e_array = []  # 最小生成樹的edge數組


class Vertex:
    def __init__(self, distance, x, y):
        self.distance = distance
        self.x = x
        self.y = y

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.x == other.y and self.y == other.x
        else:
            return False

    def __hash__(self):
        m1 = min(self.x, self.y)
        m2 = max(self.x, self.y)
        return hash(("[%s,%s,%s]" % (m1, m2, self.distance)))

    def __repr__(self):
        return "[%s,%s,%s]" % (self.x, self.y, self.distance)


def find(parent, f):
    while parent[f] > 0:
        f = parent[f]
    return f


def kruskal():
    edge_with_distance = set()
    for i, v_net in enumerate(graph):
        for j, k in enumerate(v_net):
            if i != j and k != MAX_VALUE:
                edge_with_distance.add(Vertex(k, i, j))
    # 根據distance排序,並將利用set特性將相同的節點去掉
    sorted_graph = sorted(edge_with_distance, key=lambda x: x.distance, reverse=False)
    parent = [0 for _ in range(len(graph))]
    for ve in sorted_graph:
        if len(new_v_array) == (len(graph)):
            break
        n = find(parent, ve.x)
        m = find(parent, ve.y)
        # 如果n==m,則表明存在循環節點。
        if n != m:
            parent[n] = m
            print("parent2:", parent)
            new_v_array.append(ve)
            new_e_array.append((ve.x, ve.y))
            edge_value_array.append(ve.distance)

    print("V seq:", new_v_array)
    print("E seq:", new_e_array)
    print("sum value:", sum(edge_value_array))


if __name__ == '__main__':
    kruskal()

2.1.3 結果

輸出結果與Prim結果相同

3. 總結

Prim算法以頂點爲起點,逐步尋找頂點上最小權值的邊來構建最小生成樹,就像參加世博會,先從第一個入口進,然後尋找所在場館周邊最感興趣的館,然後再用相同的辦法查看下一個。。。。。

Kruskal算法是基於權值最小的邊來構建生成樹,該思路參加世博園就是先去最想參觀的館,再參觀次想參觀的館,最終參觀完所有館。

關於算法效率,Kruskal針對邊來展開,邊少時效率非常高,是和用於稀疏圖。Prim適用於邊比較多的場景。

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