用Swift实现Dijkstra算法

· 原文地址:https://medium.com/swiftly-swift/dijkstras-algorithm-in-swift-15dce3ed0e22
· 原文作者:Federico Zanetello

如果你以前听说过图论, 那么你熟悉Dijkstra算法,
如果你不熟悉,那么好, 这篇文章包含了你所需要知道的一切

快速介绍

这个章节将带你快读过一下什么是图论和Dijkstra算法

如果你够自信,你可以跳过这个部分(直接跳到Swift章节)

图论

看到文章开头的图片了吗? 这在数学和计算机科学里面我们称作

这些圆型我们称作顶点(或者节点 ),每一图中的,都是两个定点所形成的元素对.

一般, 连接两个顶点的边都有两个主要的特征:无向有向
无向图,即两个顶点是无序的; 有向图,点是有序的,但不是反之亦然(你需要另一条反向的边).

这些简单的概念在世界范围内都有大量的应用, 你很有可能一直在使用它们

真实世界的例子

无向图:Facebook
让我们先可视化你的Facebook朋友成图
最后的结果就像这样


图来自: Mathematica StackExchange

在这张图里面每一个顶点是一个人, 并且每一个(无向)边表明这些人的朋友关系.
"很巧合",Facebook有一个面向开发者的API叫做 .
有向图:Twitter
如果我们看下我们在Twitter上面的粉丝,结果就像这样:


图来自:Social Media Research Foundation.
在这里, 每一个Twitter账号是一个顶点, 但是这些边都是单向的, 因为我关注你, 这并不表示你也关注你.

Dijkstra算法

现在我们了解了图是什么,我们来讨论下这里面最热门的话题:最短路径.

这问题的挑战也是最容易理解:在一个图里面给定两个顶点,找到一条从一个顶点到另一个顶点的最短的路径(如果存在的话).

对我们来说, 这是一个最简单的游戏(就像走迷宫一样), 但是对于机器来说, 要以最快速度的解决没那么简单

还有, 这也是你一直在用的, 现在你可以想一下,苹果地图app是怎样计算最优路线的, 或者Linkedln是怎样判定一个人和你的关系链是一级/二级/三级的.


图来自:LinkedIn
其中有一个最有名的(我敢说,是最有名的)最短路径寻找算法是Dijkstra算法,它是基于下面三步:

  1. 找到权重最小且没有访问过的顶点路径
  2. 标记已访问过,并继续访问从来没有访问过的顶点
  3. 重复上述步骤

当算法达到最终顶点或者不再有其他可到达的顶点时, 就结束了.

低权重, 意思是访问所有已访问过的订点到当前顶点并耗费最少.

花销来自于:有时候图的边相等(就像Facebook朋友关系一样,一个人与另一个人之见的边没有区别), 但是有时候它们不一样:如果你有两种方式回家,有条路相比另外条路更近,因为另一条路你可能需要爬山或者其他的.

Swift 代码实现

现在我们加快速度, 用Swift来实现所有的

Node

class Node {
   var visited = false
   var connections:[Connection] = []
}

Node更像是一个属性(算法需要),表示我们是否已经访问过它,还有一个连接其他顶点的边的数组

Connection

class Connection {
  public let to:Node
  public let weight:Int
  public init(to node:Node, weight:Int) {
    assert(weight>=0, "weight has to be equal or greater than zero"
    self.to = node
    self.weight = weight  
  }    
}

正如在Node的定义中看到,每一个connection都被赋值了一个特定的顶点,所以我们所需要做在connection中做的就是定义它的权重(也叫做花销)和它连接的那个顶点.

很明显的,我用的是有向边, 这是掌握无向图和有向图最简单的方法.

Path

最后,我们需要定义一个路径:这个路径不仅仅是匹配顶点的总和

它将会帮助我们追踪图中哪条路径被访问过和如何访问的

还有重要的是, 这个算法将会返回一个元素对, 这个元素对描述了源顶点到目的顶点的最短路径.

我们将用一种递归来定义:

class Path {
  public let cumulativeWeight:Int
  public let node:Node
  public let previousPath:Path?
  
init(to node:Node, via connection:Connection? = nil, previousPath path:Path? = nil) {
  if let previousPath = path,
    let viaConnection = connection {
    self.cumulativeWeight = viaConnection.weight + previousPath.cumulativeWeight
  } else {
    self.cumulativeWeight = 0
  }
 }
}

为了方便, 我增加了一个 cumulativeWeight 属性去记录路径顶点的权重:这个权重是所有从源顶点到目的顶点边的权重之和

算法

所有事情准备就绪, 下面我们来开始算法:

func shortestPath(source: Node, destination: Node) -> Path? {
  var frontier: [Path] = [] {
    didSet { frontier.sort { return $0.cumulativeWeight < $1.cumulativeWeight } } // the frontier has to be always ordered
  }
  
  frontier.append(Path(to: source)) // the frontier is made by a path that starts nowhere and ends in the source
  
  while !frontier.isEmpty {
    let cheapestPathInFrontier = frontier.removeFirst() // getting the cheapest path available
    guard !cheapestPathInFrontier.node.visited else { continue } // making sure we haven't visited the node already
    
    if cheapestPathInFrontier.node === destination {
      return cheapestPathInFrontier // found the cheapest path 😎
    }
    
    cheapestPathInFrontier.node.visited = true
    
    for connection in cheapestPathInFrontier.node.connections where !connection.to.visited { // adding new paths to our frontier
      frontier.append(Path(to: connection.to, via: connection, previousPath: cheapestPathInFrontier))
    }
  } // end while
  return nil // we didn't find a path 😣
} 

首先,定义一个前驱(frontier):前面访问过的顶点的路径集合.

开始是空的, 一执行这段代码我们就从开始顶点添加了一条路径(第6行代码)

我们现在开始一步一步看这个算法:

1. 找到权重最低的且没有访问过的顶点

为了达到目的, 我们从前驱(第9行)提取到了最低权重路径, 检查这个顶点是否被访问过,如果没有,我们继续下一步(第10行).

2. 标记已访问过,并继续访问从来没有访问过的顶点

当我们到这一步时, 我们需要确保顶点是被访问过的(第16行),然后,遍历这个顶点能直接到达的新的(没有访问过的)边, 并把它们都加上去(第18-20行)

3. 重复步骤

while循环现在完成了! 所以我们真的只需要重复上述两个步骤

要点 1

你可能注意到了, 第一和第二个步骤中间有些东西(12-14行):
检查新的权重最低的顶点是否是我们的目的顶点:如果是,恭喜! 我们完成了这个算法! 否则继续步骤2

要点 2

这个算法可能返回一个可选类型(第1行和第22行):
有可能源顶点和目的顶点中间不存在连接彼此的路径

Swift Playground

好的! 现在我们可以用Swift来玩玩我们的 Dijkstra 算法了!下面就是在Playground上面演示的例子.

class Node {
  var visited = false
  var connections: [Connection] = []
}

class Connection {
  public let to: Node
  public let weight: Int
  
  public init(to node: Node, weight: Int) {
    assert(weight >= 0, "weight has to be equal or greater than zero")
    self.to = node
    self.weight = weight
  }
}

class Path {
  public let cumulativeWeight: Int
  public let node: Node
  public let previousPath: Path?
  
  init(to node: Node, via connection: Connection? = nil, previousPath path: Path? = nil) {
    if
      let previousPath = path,
      let viaConnection = connection {
      self.cumulativeWeight = viaConnection.weight + previousPath.cumulativeWeight
    } else {
      self.cumulativeWeight = 0
    }
    
    self.node = node
    self.previousPath = path
  }
}

extension Path {
  var array: [Node] {
    var array: [Node] = [self.node]
    
    var iterativePath = self
    while let path = iterativePath.previousPath {
      array.append(path.node)
      
      iterativePath = path
    }
    
    return array
  }
}

func shortestPath(source: Node, destination: Node) -> Path? {
  var frontier: [Path] = [] {
    didSet { frontier.sort { return $0.cumulativeWeight < $1.cumulativeWeight } } // the frontier has to be always ordered
  }
  
  frontier.append(Path(to: source)) // the frontier is made by a path that starts nowhere and ends in the source
  
  while !frontier.isEmpty {
    let cheapestPathInFrontier = frontier.removeFirst() // getting the cheapest path available
    guard !cheapestPathInFrontier.node.visited else { continue } // making sure we haven't visited the node already
    
    if cheapestPathInFrontier.node === destination {
      return cheapestPathInFrontier // found the cheapest path 😎
    }
    
    cheapestPathInFrontier.node.visited = true
    
    for connection in cheapestPathInFrontier.node.connections where !connection.to.visited { // adding new paths to our frontier
      frontier.append(Path(to: connection.to, via: connection, previousPath: cheapestPathInFrontier))
    }
  } // end while
  return nil // we didn't find a path 😣
}

// **** EXAMPLE BELOW ****
class MyNode: Node {
  let name: String
  
  init(name: String) {
    self.name = name
    super.init()
  }
}

let nodeA = MyNode(name: "A")
let nodeB = MyNode(name: "B")
let nodeC = MyNode(name: "C")
let nodeD = MyNode(name: "D")
let nodeE = MyNode(name: "E")

nodeA.connections.append(Connection(to: nodeB, weight: 1))
nodeB.connections.append(Connection(to: nodeC, weight: 3))
nodeC.connections.append(Connection(to: nodeD, weight: 1))
nodeB.connections.append(Connection(to: nodeE, weight: 1))
nodeE.connections.append(Connection(to: nodeC, weight: 1))

let sourceNode = nodeA
let destinationNode = nodeD

var path = shortestPath(source: sourceNode, destination: destinationNode)

if let succession: [String] = path?.array.reversed().flatMap({ $0 as? MyNode}).map({$0.name}) {
  print("🏁 Quickest path: \(succession)")
} else {
  print("💥 No path between \(sourceNode.name) & \(destinationNode.name)")
}

总结

在学术界里有种议论, 就是在高校课程里是否可以将 几何 替换成 图论 :作为一个计算机工程师,我认为未来可能会发生哦😁.

我希望你在新的一天能学到一些东西, 下次再见👋

推荐阅读更多精彩内容