《swift-algorithm-club》——数据结构/图

638 阅读8分钟

图(Graphs)

图(Graph)

在计算机科学中,图形被定义为一组和与之配对的一组。边可以具有权重,也可以有向的。

在代码中描述图,有两种主要策略;邻接表和邻接矩阵

邻接表(Adjacency List)。在邻接表实现中,每个点存储一个从这个点出发的所有边的列表。

邻接矩阵(Adjacency Matrix)。在邻接矩阵实现中,具有表示顶点的行和列的矩阵存储权重以指示顶点是否链接以及权重。

V是图中点的数量,E是边数。我们有

操作邻接表邻接矩阵
存储空间O(V + E)O(V^2)
添加点O(1)O(V^2)
Add EdgeO(1)O(1)
添加边O(1)O(1)
检查邻接O(V)O(1)
public struct Edge<T>: Equatable where T: Equatable, T: Hashable {
  
  public let from: Vertex<T>
  public let to: Vertex<T>
  
  public let weight: Double?
  
}
public struct Vertex<T>: Equatable where T: Equatable, T: Hashable {
  public var data: T
  public let index: Int
}

代码:邻接表

private class EdgeList<T> where T: Equatable, T: Hashable {
	var vertex: Vertex<T>
  var edges: [Edge<T>]? = nil
  
  init(vertex: Vertex<T>) {
    self.vertex = vertex
  }
  
  func addEdge(_ edge: Edge<T>) {
    edges.append(edge)
  }
}
open override func createVertex(_ data: T) -> Vertex<T> {
  // check if the vertex already exists
  let matchingVertices = vertices.filter() { vertex in
  	return vertex.data = data
  }
  
  if matchingVertices.count > 0 {
    return matchingVertices.last!
  }
  
  // if the vertex doesn't exist, creat a new one
  let vertex = Vertex(data: data, index: adjacencyList.count)
  adjcencyList.append(EdgeList(vertex: vertex))
  return vertex
}

代码:邻接矩阵

open override func createVertex(_ data: T) -> Vertex<T> {
  // check if the vertex already exist
  let matchingVertices = vertices.filter() { vertex in
    return vertex.data = data
  }
  
  if matchingVertices.count > 0 {
    return matchingVertices.last!
  }
  
  // if the vertex doesn't exist, create a new one
  let vertex = Vertex(data: data, index: adjacencyMatrix.count)
  
  // Expand each existing row to the right one column.
  for i in 0 ..< adjacencyMatrix.count {
    adjacencyMatrix[i].append(nil)
  }
  
  // Add one new row at the bottom.
  let newRow = [Double?](repeating: nil, count: adjacencyMatrix.count + 1)
  adjacencyMatrix.append(newRow)
  
  _vertices.append(vertex)
  
  return vertex
}

广度优先搜索

队列的实现

func breadthFirstSearch(_ graph: Graph, source: Node) -> [String] {
  var queue = Queue<Node>()
  queue.enqueue(source)
  
  var nodesExplored = [source.label]
  source.visited = true
  
  while let node = queue.dequeue() {
    for edge in node.neighbors {
      let neighborNode = edge.neighbor
      if !neighborNode.visited {
        queue.enqueue(neighborNode)
        beighborNode.visited = true
        nodesExplored.append(neighborNode.label)
      }
    }
  }
  
  return nodesExplored
}

BFS可以用于解决

  • 计算源节点和其他每个节点之间的最短路径 (仅适用于未加权的图形)。
  • 在未加权的图表上计算最小生成树

深度优先搜索

简单递归实现

func depthFirstSearch(_ graph: Graph, source: Node) -> [String] {
  var nodesExpored = [Source.label]
  source.visited = true
  
  for edge in source.neighbors {
    if !edge.neighbor.visited {
      nodesExplored += depthFirstSearch(graph, source: edge.neighbor)
    }
  }
  return nodesExplored
}

也可以用实现

DFS可以用于解决

  • 查找稀疏图的连通分量
  • 图中节点的拓扑排序
  • 查找图的桥梁
  • more

未加权图的最短路径

目标:找到图中从一个节点到另一个节点的最短路径

未加权图:广度优先搜索

func breadthFirstSearchShortestPath(graph: Graph, source: Node) -> Graph {
  let shortestPathGraph = graph.duplicate()

  var queue = Queue<Node>()
  let sourceInShortestPathsGraph = shortestPathGraph.findNodeWithLabel(label: source.label)
  queue.enqueue(element: sourceInShortestPathsGraph)
  sourceInShortestPathsGraph.distance = 0

  while let current = queue.dequeue() {
    for edge in current.neighbors {
      let neighborNode = edge.neighbor
      if !neighborNode.hasDistance {
        queue.enqueue(element: neighborNode)
        neighborNode.distance = current.distance! + 1
      }
    }
  }

  return shortestPathGraph
}

单源最短路径

单源最短路径问题是从一个给定的源顶点到有向加权图中其他所有顶点的最短路径。

Bellman-Ford

u是源顶点,v是有向边的目标顶点,边e = (u, v)

u保持0,其他顶点的初始值为♾

if weights[v] > weights[u] + e.weight {
  weights[v] = weights[u] + e.weight
}

未加权图的最小生成树树

深度优先搜索

func breadthFirstSearchMinimumSpanningTree(graph: Graph, source: Node) -> Graph {
  let minimumSpanningTree = graph.duplicate()
  
  var queue = Queue<Node>()
  let sourceInMinimumSpanningTree = minimumSpanningTree.findNodeWithLabel(source.label)
  queue.enqueue(sourceInMinimumSpanningTree)
  sourceInMinimumSpanningTree.visited = true
  
  while let current = queue.dequeue() {
    for edge in current.neighbors {
      let neighborNode = edge.neighbor
      if !neighborNode.visited {
        neighborNode.visited = true
        queue.enqueue(neighborNode)
      } else {
        current.remove(edge)
      }
    }
  }
  
  return minimumSpanningTree
}

最小生成树

Kruskal算法

根据权重对边进行排序。每次贪婪地选择最小的一个并且只要它不形成环就加入MST。

准备

// Initialize the values to be returned and Union Find data structure.
var cost: Int = 0
var tree = Graph<T>()
var unionFind = UnionFind<T>()
for vertex in graph.vertices {

// Initially all vertices are disconnected.
// Each of them belongs to it's individual set.
  unionFind.addSetWith(vertex)
}

排序边:

let sortedEdgeListByWeight = graph.edgeList.sorted(by: { $0.weight < $1.weight })

一次取一个边并尝试将其插入MST。

for edge in sortedEdgeListByWeight {
  let v1 = edge.vertex1
  let v2 = edge.vertex2 
  
  // Same set means the two vertices of this edge were already connected in the MST.
  // Adding this one will cause a cycle.
  if !unionFind.inSameSet(v1, and: v2) {
    // Add the edge into the MST and update the final cost.
    cost += edge.weight
    tree.addEdge(edge)
    
    // Put the two vertices into the same set.
    unionFind.unionSetsContaining(v1, and: v2)
  }
}

Prim算法

准备

// Initialize the values to be returned and Priority Queue data structure.
var cost: Int = 0
var tree = Graph<T>()
var visited = Set<T>()

// In addition to the (neighbour vertex, weight) pair, parent is added for the purpose of printing out the MST later.
// parent is basically current vertex. aka. the previous vertex before neigbour vertex gets visited.
var priorityQueue = PriorityQueue<(vertex: T, weight: Int, parent: T?)>(sort: { $0.weight < $1.weight })

排序顶点:

priorityQueue.enqueue((vertex: graph.vertices.first!, weight: 0, parent: nil))
// Take from the top of the priority queue ensures getting the least weight edge.
while let head = priorityQueue.dequeue() {
  let vertex = head.vertex
  if visited.contains(vertex) {
    continue
  }

  // If the vertex hasn't been visited before, its edge (parent-vertex) is selected for MST.
  visited.insert(vertex)
  cost += head.weight
  if let prev = head.parent { // The first vertex doesn't have a parent.
    tree.addEdge(vertex1: prev, vertex2: vertex, weight: head.weight)
  }

  // Add all unvisted neighbours into the priority queue.
  if let neighbours = graph.adjList[vertex] {
    for neighbour in neighbours {
      let nextVertex = neighbour.vertex
      if !visited.contains(nextVertex) {
        priorityQueue.enqueue((vertex: nextVertex, weight: neighbour.weight, parent: vertex))
      }
    }
  }
}

任意两点间的最短路径

任意两点间的最短路径同时计算图中每个节点到其他节点的最短路径。

Floyd-Warshall算法

dij(k)={wij,k=0min(dij(k1),dik(k1)+dkj(k1)),k1d_{ij}^{(k)} = \left\{ \begin{aligned} & w_{ij} & ,k=0\\ & min(d_{ij}^{(k-1)},d_{ik}^{(k-1)}+d_{kj}^{(k-1)}) & ,k\ge1\\ \end{aligned} \right.

通俗一点,邻接矩阵记录了任意两点之间的直接距离。依次,允许经过第一个顶点 ,更新一下邻接矩阵;允许经过第一和第二个点,更新一下邻接矩阵...

Dijkstra 最短路径算法

当你有一个源顶点并且想要找到图中所有其他顶点的最短路径时,可以使用它。

首先,创建一个类来描述图中的任何顶点

open class Vertex {
  
  //Every vertex should be unique that's why we set up identifier
  open var identifier: String
  
  //For Dijkstra every vertex in the graph should be connect with at least one other vertex.But there can be some usecases
  //when you firstly initialize all vertices without neighbours. And then on next iteration you set up their neighbours. So, initially neighbours is an empty array.
  //Array contains tuples (Vertex, Double). Vertex is a neighbour and and Double is as edge weight to that neighbour.
  open var neighbours: [(Vertex, Double)] = []
  
  //As it was mentioned in the algorithm description, default path length from start for all vertices should be as much as possible.
  //It is var because we will update it during the algorith execution.
  open var pathLengthFromStart = Double.infinity
  
  //This array contains vertices which we need to go through to reach this vertex from starting one
  //As with path length from start, we will change this array during the algorithm execution.
  open var pathVerticesFromStart: [Vertex] = []
  
  public init(identifier: String) {
    self.identifier = identifier
  }
  
  //This function let us use the same array of vertices again and again to calculate paths with different starting vertex.
  //When we will need to set new starting vertex and recalculate paths then we will simply clear graph vertices' cashes.
  open func clearCache() {
    pathLengthFromStart = Double.infinity
    pathVerticesFromStart = []
  }
}

由于每个顶点都应该是唯一的,因此将它们设为Hashable并根据Equatable非常有用。

extension Vertex: Hashable {
  open var hashValue: Int {
    return identifier.hashValue
  }
}

extension Vertex: Equatable {
  public static func ==(lhs: Vertex, res: Vertex) -> Bool {
    return lhs.hashValue == rhs.hashValue
  }
}
public class Dijkstra {
    //This is a storage for vertices in the graph.
    //Assuming that our vertices are unique we can use Set instead of array. This approach will bring some benefits later.
    private var totalVertices: Set<Vertex>

    public init(vertices: Set<Vertex>) {
        totalVertices = vertices
    }

    //Remember clearCache function in the Vertex class implementation?
    //This is just a wrapper that cleans cache for all stored vertices.
    private func clearCache() {
        totalVertices.forEach { $0.clearCache() }
    }

    public func findShortestPaths(from startVertex: Vertex) {
	//Before we start searching the shortest path from startVertex,
	//we need to clear vertices cache just to be sure that out graph is clean.
	//Remember that every Vertex is a class and classes are passed by reference.
	//So whenever you change vertex outside of this class it will affect this vertex inside totalVertices Set
        clearCache()
	//Now all our vertices have Double.infinity pathLengthFromStart and an empty pathVerticesFromStart array.

	//The next step in the algorithm is to set startVertex pathLengthFromStart and pathVerticesFromStart
        startVertex.pathLengthFromStart = 0
        startVertex.pathVerticesFromStart.append(startVertex)

	//Here starts the main part. We will use while loop to iterate through all vertices in the graph.
	//For this purpose we define currentVertex variable which we will change in the end of each while cycle.
        var currentVertex: Vertex? = startVertex

        while let vertex = currentVertex {

    	    //Next line of code is an implementation of setting vertex as visited.
    	    //As it has been said, we should check only unvisited vertices in the graph,
	    //So why don't just delete it from the set? This approach let us skip checking for *"if !vertex.visited then"*
            totalVertices.remove(vertex)

	    //filteredNeighbours is an array that contains current vertex neighbours which aren't yet visited
            let filteredNeighbours = vertex.neighbours.filter { totalVertices.contains($0.0) }

	    //Let's iterate through them
            for neighbour in filteredNeighbours {
		//These variable are more representative, than neighbour.0 or neighbour.1
                let neighbourVertex = neighbour.0
                let weight = neighbour.1

		//Here we calculate new weight, that we can offer to neighbour.
                let theoreticNewWeight = vertex.pathLengthFromStart + weight

		//If it is smaller than neighbour's current pathLengthFromStart
		//Then we perform this code
                if theoreticNewWeight < neighbourVertex.pathLengthFromStart {

		    //set new pathLengthFromStart
                    neighbourVertex.pathLengthFromStart = theoreticNewWeight

		    //set new pathVerticesFromStart
                    neighbourVertex.pathVerticesFromStart = vertex.pathVerticesFromStart

		    //append current vertex to neighbour's pathVerticesFromStart
                    neighbourVertex.pathVerticesFromStart.append(neighbourVertex)
                }
            }

	    //If totalVertices is empty, i.e. all vertices are visited
	    //Than break the loop
            if totalVertices.isEmpty {
                currentVertex = nil
                break
            }

	    //If loop is not broken, than pick next vertex for checkin from not visited.
	    //Next vertex pathLengthFromStart should be the smallest one.
            currentVertex = totalVertices.min { $0.pathLengthFromStart < $1.pathLengthFromStart }
        }
    }
}

Dijkstra算法是基于【广度优先】【贪心】【动态规划】。

Bellman-Ford的优势是可以用来判断是否存在负环。

A* 算法

墙裂推荐 www.redblobgames.com/pathfinding…

A*算法通过下面这个函数来计算每个节点的优先级

f(n)=g(n)+h(n)f(n) = g(n) + h(n)

其中:

  • f(n) 是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
  • g(n)是节点n距离起点的代价。
  • h(n)是节点n距离终点的预计代价,这也就是A*算法的启发函数。

这个公式遵循以下特性:

  • 如果g(n)为0,则算法转化为使用贪心策略的最良优先搜索,速度最快,但可能得不到最优解
  • 如果h(n)不大于顶点n到目标顶点的实际距离,则一定可以求出最优解,而且h(n)越小,需要计算的节点越多,算法效率越低,常见的评估函数有——欧几里得距离、曼哈顿距离、切比雪夫距离
  • 如果h(n)为0,则转化为单源最短路径问题(SSSP),即Dijkstra算法,此时需要计算最多的顶点;
//Matlab語言
 function A*(start,goal)
     closedset := the empty set                 //已经被估算的節點集合
     openset := set containing the initial node //將要被估算的節點集合,初始只包含start
     came_from := empty map
     g_score[start] := 0                        //g(n)
     h_score[start] := heuristic_estimate_of_distance(start, goal)    //通過估計函數 估計h(start)
     f_score[start] := h_score[start]            //f(n)=h(n)+g(n),由於g(n)=0,所以省略
     while openset is not empty                 //當將被估算的節點存在時,執行循環
         x := the node in openset having the lowest f_score[] value   //在將被估計的集合中找到f(x)最小的節點
         if x = goal            //若x為終點,執行
             return reconstruct_path(came_from,goal)   //返回到x的最佳路徑
         remove x from openset      //將x節點從將被估算的節點中刪除
         add x to closedset      //將x節點插入已經被估算的節點
         for each y in neighbor_nodes(x)  //循環遍歷與x相鄰節點
             if y in closedset           //若y已被估值,跳過
                 continue
             tentative_g_score := g_score[x] + dist_between(x,y)    //從起點到節點y的距離

             if y not in openset          //若y不是將被估算的節點
                 tentative_is_better := true     //暫時判斷為更好
             elseif tentative_g_score < g_score[y]         //如果起點到y的距離小於y的實際距離
                 tentative_is_better := true         //暫時判斷為更好
             else
                 tentative_is_better := false           //否則判斷為更差
             if tentative_is_better = true            //如果判斷為更好
                 came_from[y] := x                  //將y設為x的子節點
                 g_score[y] := tentative_g_score    //更新y到原點的距離
                 h_score[y] := heuristic_estimate_of_distance(y, goal) //估計y到終點的距離
                 f_score[y] := g_score[y] + h_score[y]
                 add y to openset         //將y插入將被估算的節點中
     return failure
 
 function reconstruct_path(came_from,current_node)
     if came_from[current_node] is set
         p = reconstruct_path(came_from,came_from[current_node])
         return (p + current_node)
     else
         return current_node

快速扩展随机树(Rapidly-Exploring Random Trees, RRT)

RRT.png