最小生成樹MST算法(Prim、Kruskal)


最小生成樹MST(Minimum Spanning Tree)

(1)概念

一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 n 個結點,並且有保持圖連通的最少的邊,所謂一個 帶權圖 的最小生成樹,就是原圖中邊的權值最小的生成樹 ,所謂最小是指邊的權值之和小於或者等於其它生成樹的邊的權值之和。

(2)性質

  • 一個連通圖可以有多個生成樹;

  • 一個連通圖的所有生成樹都包含相同的頂點個數和邊數;

  • 生成樹當中不存在環;

  • 移除生成樹中的任意一條邊都會導致圖的不連通, 生成樹的邊最少特性;

  • 在生成樹中添加一條邊會構成環。

  • 對於包含n個頂點的連通圖,生成樹包含n個頂點和n-1條邊;

  • 對於包含n個頂點的無向完全圖最多包含 [公式] 顆生成樹。

(3)應用

例如:要在n個城市之間鋪設光纜,主要目標是要使這 n 個城市的任意兩個之間都可以通信,但鋪設光纜的費用很高,且各個城市之間鋪設光纜的費用不同,因此另一個目標是要使鋪設光纜的總費用最低。這就需要找到帶權的最小生成樹

MST算法之Prim

算法參考地址:Prim的最小生成樹(MST)|貪婪的算法-5 - 極客 (geeksforgeeks.org)

 

Prim算法的流程

1) 創建一組 mstSet,用於跟蹤 MST 中已包含的頂點。 2) 為輸入圖中的所有頂點分配一個鍵值。將所有鍵值初始化為 INFINITE。為第一個頂點分配鍵值為 0,以便首先選取它。 3) 雖然 mstSet 不包括所有頂點 ....a) 選擇一個在 mstSet 中不存在且具有最小鍵值的頂點 u ....b) 將 u 包含在 mstSet 中。 ....c) 更新 u 的所有相鄰頂點的鍵值。要更新鍵值,請循環訪問所有相鄰的頂點。對於每個相鄰的頂點 v,如果邊 u-v 的權重小於 v 的前一個鍵值,則將鍵值更新為 u-v 的權重使用鍵值的想法是從剪切中選取最小權重邊。鍵值僅用於尚未包含在 MST 中的折點,這些折點的鍵值表示將它們連接到 MST 中包含的折點集的最小權重邊。

讓我們通過以下示例來理解:

Prim 的最小生成樹

設置的 mstSet 最初是空的,分配給頂點的鍵是 {0, INF, INF, INF, INF, INF, INF, INF},其中 INF 表示無限。現在選取具有最小鍵值的頂點。選取頂點 0,將其包含在 mstSet 中。因此,mstSet 變得{0}。包含到 mstSet 后,更新相鄰頂點的鍵值。相鄰頂點 0 為 1 和 7。1 和 7 的鍵值將更新為 4 和 8。下圖顯示頂點及其鍵值,僅顯示具有有限鍵值的頂點。MST 中包含的頂點以綠色顯示。

Prim 的最小生成樹算法 1

選取具有最小鍵值且尚未包含在 MST 中(不在 mstSET 中)的頂點。選取頂點 1 並將其添加到 mstSet。所以 mstSet 現在變成 {0, 1}。更新相鄰頂點 1 的鍵值。頂點 2 的鍵值變為 8。

Prim 的最小生成樹算法 2

選取具有最小鍵值且尚未包含在 MST 中(不在 mstSET 中)的頂點。我們可以選擇頂點7或頂點2,讓頂點7被選中。所以 mstSet 現在變成 {0, 1, 7}。更新相鄰頂點 7 的鍵值。頂點 6 和 8 的鍵值變為有限(分別為 1 和 7)。

Prim 的最小生成樹算法 3

選取具有最小鍵值且尚未包含在 MST 中(不在 mstSET 中)的頂點。選取頂點 6。所以 mstSet 現在變成 {0, 1, 7, 6}。更新相鄰頂點 6 的鍵值。頂點 5 和 8 的鍵值將更新。

Prim 的最小生成樹算法 4

我們重復上述步驟,直到 mstSet 包含給定圖形的所有頂點。最后,我們得到下圖。

Prim 的最小生成樹算法 5

 

Prim算法的實現(golang)

prim算法的思想和Dijkstra很相似,在理解Dijkstra算法的前提下,理解Prim算法及其實現都會變得非常容易

//graph 中值為math.MaxInt的值為不可達
func prim(graph [][]int, randomVertex int) int {
  n := len(graph)
  //圖中已經遍歷到的頂點到未遍歷的頂點的最短的距離
  dist := make([]int, n)
  //圖中的頂點是否被訪問過
  visit := make([]bool, n)
  //最小生成書的路徑和
  res := 0

  curIdx := randomVertex

  //標記初始訪問節點
  visit[curIdx] = true
  //初始化當前節點到未訪問節點的距離
  for i := 0; i < n; i++ {
     dist[i] = graph[curIdx][i]
  }

  //由於已經初始化一個節點,所以只需便利n-1次
  for i := 1; i < n; i++ {
     minor := math.MaxInt
     for j := 0; j < n; j++ {
        //尋找與已存在節點相接的最短距離的節點
        if !visit[j] && dist[j] < minor {
           minor = dist[j]
           curIdx = j
        }
    }

     //標記到最短距離的節點為已訪問
     visit[curIdx] = true
     //最短路徑值求和
     res += minor

     //重新初始化已訪問節點到未訪問節點的距離
     for j := 0; j < n; j++ {
        /**
        僅更新沒有訪問過的節點且節點小於當前距離的節點
        (因為如果graph[curIdx][j]> dist[j]的話,說明當前已經有節點到節點j的距離更小,
        所以此邊(graph[curIdx][j])永遠也不會被用到)
        */
        if !visit[j] && graph[curIdx][j] < dist[j] {
           dist[j] = graph[curIdx][j]
        }
    }
  }
  return res
}

堆優化版的Prim算法


// Edge 最小生成樹prim算法(尋找已知節點到位置節點的最小路徑用堆優化)
//graph 中值為math.MaxInt的值為不可達
type Edge struct {
  startVertex int
  endVertex   int
  weight      int
}
type EdgeHeap []Edge

func (h EdgeHeap) Len() int           { return len(h) }
func (h EdgeHeap) Less(i, j int) bool { return h[i].weight < h[j].weight }
func (h EdgeHeap) Swap(i, j int)     { h[i], h[j] = h[j], h[i] }

func (h *EdgeHeap) Push(x interface{}) {
  *h = append(*h, x.(Edge))
}
func (h *EdgeHeap) Pop() interface{} {
  n := len(*h)
  res := (*h)[n-1]
  *h = (*h)[:n-1]
  return res
}

func primHeap(graph [][]int, randomVertex int) int {
  //F代表兩點之間不可達
  const F = math.MaxInt
  n := len(graph)
  //圖中已經遍歷到的頂點到未遍歷的頂點的最短的距離
  distHeap := make(EdgeHeap, n)
  //圖中的頂點是否被訪問過
  visit := make([]bool, n)
  //最小生成書的路徑和
  res := 0
  //節點訪問數
  count := 1

  curIdx := randomVertex

  //標記初始訪問節點
  visit[curIdx] = true
  //初始化當前節點到未訪問節點的距離
  for i := 0; i < n; i++ {
     if graph[curIdx][i] != F {
        distHeap[i] = Edge{curIdx, i, graph[curIdx][i]}
    }
  }
  heap.Init(&distHeap)
  for len(distHeap) > 0 && count < n {
     edge := heap.Pop(&distHeap).(Edge)
     //兩個頂點都已訪問過的話,說明如果在加入該條邊就構成環,所以跳過
     if visit[edge.startVertex] && visit[edge.endVertex] {
        continue
    }
     if !visit[edge.startVertex] {
        visit[edge.startVertex] = true
        count++
        res += edge.weight
        for i := 0; i < n; i++ {
           if !visit[i] {
              heap.Push(&distHeap, Edge{edge.startVertex, i, graph[edge.startVertex][i]})
          }
        }
    } else {
        count++
        visit[edge.endVertex] = true
        res += edge.weight
        for i := 0; i < n; i++ {
           if !visit[i] {
              heap.Push(&distHeap, Edge{edge.endVertex, i, graph[edge.endVertex][i]})
          }
        }
    }
  }
  return res
}

MST算法之Kruskal

算法參考地址: https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-algorithm-greedy-algo-2/

前置知識:由於Kruskal使用並查集算法來檢測圖中是否存在環。因此,我們建議閱讀以下文章作為先決條件。 聯合查找算法|設置 1(檢測圖形中的周期) 並集查找算法|集合 2(按秩和路徑壓縮並集)

不了解的同學可以也參考:https://blog.csdn.net/the_zed/article/details/105126583,該博主以我們熟悉的故事為主題使我們更加容易的學習和了解並查集算法.

算法的流程

1. 按其權重的非遞減順序對所有邊進行排序。 2. 選擇最小的邊緣。檢查它是否與到目前為止形成的生成樹形成一個循環。如果未形成循環,則包括此邊。否則,丟棄它。 3. 重復步驟#2,直到生成樹中有(V-1)條邊。

該算法是貪婪算法。貪婪的選擇是選擇在迄今為止構建的MST中不會導致循環的最小重量邊緣。讓我們通過一個例子來理解它:考慮下面的輸入圖。

Kruskal的最小生成樹算法

該圖包含 9 個頂點和 14 條邊。因此,形成的最小生成樹將具有(9 – 1)= 8條邊。

After sorting:

Weight   Src   Dest
1         7     6
2         8     2
2         6     5
4         0     1
4         2     5
6         8     6
7         2     3
7         7     8
8         0     7
8         1     2
9         3     4
10       5     4
11       1     7
14       3     5

現在,從排序的邊列表中逐個選取所有邊 1。*選取邊 7-6:*未形成循環,請將其包括在內。

Kruskal的最小生成樹算法

2.*拾取邊緣8-2:*不形成循環,包括它。

Kruskal的最小生成樹算法

3.*拾取邊緣6-5:*不形成循環,包括它。

Kruskal的最小生成樹算法

4.*拾取邊緣0-1:*不形成循環,包括它。

Kruskal的最小生成樹算法

5.*拾取邊緣2-5:*不形成循環,包括它。

Kruskal的最小生成樹算法

6. 拾取邊緣 8-6:由於包含此邊緣會導致循環,因此請將其丟棄。 7.*拾取邊緣2-3:*不形成循環,包括它。

Kruskal的最小生成樹算法

8. 拾取邊緣 7-8:由於包含此邊緣會導致循環,因此請將其丟棄。 9.*拾取邊緣0-7:*不形成循環,包括它。

Kruskal的最小生成樹算法

10. 選取邊緣 1-2:由於包含此邊緣會導致循環,因此請將其丟棄。 11.*拾取邊緣3-4:*不形成循環,包括它。

Kruskal的最小生成樹算法

由於包含的邊數等於 (V – 1),因此算法在此處停止。

算法的實現(golang)

type Edge struct {
startVertex int
endVertex   int
weight      int
}
type EdgeHeap []Edge

func (h EdgeHeap) Len() int           { return len(h) }
func (h EdgeHeap) Less(i, j int) bool { return h[i].weight < h[j].weight }
func (h EdgeHeap) Swap(i, j int)     { h[i], h[j] = h[j], h[i] }

func (h *EdgeHeap) Push(x interface{}) {
*h = append(*h, x.(Edge))
}
func (h *EdgeHeap) Pop() interface{} {
n := len(*h)
res := (*h)[n-1]
*h = (*h)[:n-1]
return res
}

//並查集
var tree []int

//初始化並查集
func initTree(n int) {
  tree = make([]int, n)
  for i := 0; i < n; i++ {
     tree[i] = i
  }
}

//返回當前節點的頭結點
func search(a int) int {
  if a == tree[a] {
     return a
  } else {
     //壓縮路徑
     tree[a] = search(tree[a])
     return tree[a]
  }
}

func union(a, b int) {
  rootA := search(a)
  rootB := search(b)
  if rootA == rootB {
     //a和b已經在同一顆樹上
     return
  }
  //暫定誰的數值大誰就是老大
  if rootA > rootB {
     tree[rootB] = rootA
  } else {
     tree[rootA] = rootB
  }
}
func Kruskal(graph [][]int) int {
  //F代表兩點之間不可達
  const F = math.MaxInt
  n := len(graph)
  initTree(n)
  //圖中已經遍歷到的頂點到未遍歷的頂點的最短的距離
  distHeap := make(EdgeHeap, n)
  //已經遍歷的邊的數量
  count := 0
  //最小生成書的路徑和
  res := 0

  for i := 0; i < n; i++ {
     for j := i + 1; j < n; j++ {
        distHeap = append(distHeap, Edge{i, j, graph[i][j]})
    }
  }
  heap.Init(&distHeap)

  //邊的數目為n-1的時候即為最小生成樹
  for count < n && len(distHeap) > 0 {
     edge := heap.Pop(&distHeap).(Edge)
     if search(edge.startVertex) != search(edge.endVertex) {
        count++
        union(edge.startVertex, edge.endVertex)
        res += edge.weight
    }
  }
  return res
}

總結

最小生成樹(Minimum Spanning Tree)算法在我們的實際中有很多的應用,因此掌握最小生成樹算法是非常有必要的,而最小生成樹又有兩種實現(Prim和Kruskal),這兩種算法並沒有什么優劣之分,從兩種算法的實現可以看出,Prim算法是以頂點為基礎,一點一點向外延申,直到所有的頂點都便利完成,算法結束,得到最小生成樹,而Kruskal是以邊為基礎向外擴展,知道有n-1條邊,算法結束,得到最小生成樹.所以我們可以得到(假如有兩棵頂點樹相同的樹)Prim更適用於邊數較多的圖(稠密圖),而Kruskal更適用於邊數較少的圖 (稀疏圖).


免責聲明!

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



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