美文网首页
Prim算法

Prim算法

作者: Bel李玉 | 来源:发表于2020-08-12 00:02 被阅读0次

    在之前的文章里,我们共同探讨了广度优先搜索算法和深度优先搜索算法,这些算法构成了生成树。
    生成树是无向图的一个子图,包含图的所有顶点,并且使用最小数量的边。它不能包含一个环,其不能不相连。
    下图是一些生成树的示例:


    Prim1.png

    从这个无向图中,可以得到3个不同的生成树,只需要两条边就可以连接所有的点。

    在这篇文章中,我们将探索 Prim算法,一个贪婪算法,经常使用它来构建 最小生成树

    最小生成树是一个总的边的权值最小的生成树。举个例子,你可以使用它找到铺设水管时的最短路径。

    下图是一个有权的无向图的最小生成树:


    Prim2.png

    注意只有第三个子图是最小生成树,因为它的总花费最少为3。

    Prim算法是通过每次都选择权值最小的边来生成最小生成树的,下面的图通过6步,就可以得到最小生成树。


    Prim3.png

    示例

    将下图想象成一个机场的航线图。图中顶点是机场,图中的边表示该航线从一个机场到另一个机场所花费的油费。

    Prim4.png
    我们从该图开始分析:
    Prim5.png
    1,选择图中任意一个顶点,在这里我们选择顶点2。
    2,这个顶点的边的权重为[6,5,3]。贪婪算法选择权值最小的边。
    3,选择权重为3的边,并且将它和顶点5连接。 Prim6.png
    1,已经访问的顶点是{2,5}
    2,从已访问顶点的所有边中,选择一个最小的边。所有的边是{6,5,6,6},我们选择权重为5的边,它和顶点3相连。
    3,这里需要注意的是 顶点5和顶点3之间的边可以移除了,顶点{2, 3, 5}现在已经是生成树的一部分。
    Prim7.png
    1,现在已访问的顶点是{2, 3, 5}
    2,现在潜在的边的是{6, 1, 5, 4, 6},我们选择连接顶点1,权重为顶点1的边。
    3,顶点2顶点1之间的边现在可以移除掉了。
    Prim8.png
    1,已访问的顶点是{2,3,5,1}
    2,在[5,5,4,6]中选择最短的边,此时,我们选择连接顶点6权重为4的边。
    3,移除顶点5顶点6 之间的边。
    Prim10.png
    1,现在已访问的顶点是 {2,5,3,1,6}
    2,在与已访问顶点的边中选择最短的边。现在相关的边是[5, 5, 2]
    3,删除连接顶点4顶点1顶点6的边。
    ⚠️如果所有的边有相同的权重值,我们可以从中任意选择一个。
    Prim11.png
    上图就是我们使用 Prim算法得到的最小生成树。

    实现

    我们先添加如下代码:

    public class Prim<T: Hashable> {
    
      public typealias Graph = AdjacencyList<T>
      public init() {}
    }
    

    Graph定义为 AdjacencyList的别名。

    辅助方法

    在开始实现该算法之前,我们来创建一个辅助方法,避免重复的代码。

    复制图
    public func copyVertices(from graph: AdjacencyList) {
      for vertex in graph.vertices {
        adjacencies[vertex] = []
      }
    }
    

    把邻接表中所有的顶点复制到新的邻接表中

    查找边

    除了复制图中所有的顶点之外,我们需要查找并存储已经访问过的顶点所相关的边。

    internal func addAvailableEdges(
        for vertex: Vertex<T>,
        in graph: Graph,
        check visited: Set<Vertex<T>>,
        to priorityQueue: inout PriorityQueue<Edge<T>>) {
      for edge in graph.edges(from: vertex) { // 1
        if !visited.contains(edge.destination) { // 2
          priorityQueue.enqueue(edge) // 3
        }
      }
    }
    

    这个方法包含4个参数:
    1,当前的顶点。
    2,graph 用来存储当前顶点。
    3,已经访问的顶点。
    4,优先级队列来存放潜在的边。

    该方法做了以下事情:
    1,查找和当前顶点相关联的边。
    2,检查 目的顶点是否已经被访问过。
    3,如果没有被访问过,就把它加到优先级队列中。

    创造最小生成树

    Prim类中添加如下代码:

    public func produceMinimumSpanningTree(for graph: Graph)
        -> (cost: Double, mst: Graph) { // 1
      var cost = 0.0 // 2
      let mst = Graph() // 3
      var visited: Set<Vertex<T>> = [] // 4
      var priorityQueue = PriorityQueue<Edge<T>>(sort: { // 5
        $0.weight ?? 0.0 < $1.weight ?? 0.0
      })
      // to be continued
    }
    

    1,produceMinimumSpanningTree方法将 无向图作为参数,并且返回一个包含最小生成树和其总权重的元组。
    2,cost用来记录最小生成树总的权重。
    3,这个图将会变为最小生成树。
    4,visited 用来存储已经访问过的顶点。
    5,使用最小优先级队列来存储边,

    接下来 produceMinimumSpanningTree中增加如下代码:

    mst.copyVertices(from: graph) // 1
    
    guard let start = graph.vertices.first else { // 2
      return (cost: cost, mst: mst)
    }
    
    visited.insert(start) // 3
    addAvailableEdges(for: start, // 4
                       in: graph,
                    check: visited,
                       to: &priorityQueue)
    

    1,将图中所有的顶点拷贝到最小生成树的图中。
    2,从图中选择一个起始顶点。
    3,将起始顶点标记为已访问过。
    4,将所有已访问过顶点的边,加入到最低优先级队列中。

    最后,完善 produceMinimumSpanningTree 方法:

    while let smallestEdge = priorityQueue.dequeue() { // 1
      let vertex = smallestEdge.destination // 2
      guard !visited.contains(vertex) else { // 3
        continue
      }
    
      visited.insert(vertex) // 4
      cost += smallestEdge.weight ?? 0.0 // 5
    
      mst.add(.undirected, // 6
              from: smallestEdge.source,
              to: smallestEdge.destination,
              weight: smallestEdge.weight)
    
      addAvailableEdges(for: vertex, // 7
                        in: graph,
                        check: visited,
                        to: &priorityQueue)
    }
    
    return (cost: cost, mst: mst) // 8 
    

    1,如果队列不为空,继续该算法。
    2,获得最小边的目的顶点。
    3,如果该顶点已经访问过,则继续该循环。
    4,将顶点标记为已经访问过。
    5,将边的权重加到总的 cost 里面。
    6,将最小边加到最小生成树的图中。
    7,将当前顶点所有的边加到优先级队列中。
    8,一旦 priorityQueue为空,则返回总的最小权重和最小生成树。

    最后附上本文的相关代码DataAndAlgorim

    参考链接《Data Structures & Algorithms in Swift》

    相关文章

      网友评论

          本文标题:Prim算法

          本文链接:https://www.haomeiwen.com/subject/qaqudktx.html