跳转至

41.深度优先搜索的挑战

挑战1:BFSDFS

对于以下两个例子,哪种遍历方法(深度优先或广度优先)更有利于发现两个节点之间是否存在路径?解释一下原因。

img

  • AF的路径。
  • AG的路径。

挑战2:递归DFS

在本章中,你讲了深度优先搜索的迭代实现。现在写一个递归实现。

挑战3:检测循环

Graph添加一个方法,检测一个directed图是否有一个周期。

解决方案

挑战1的解决方案

  • AF的路径。使用深度优先,因为你要找的路径在图中比较深。
  • AG的路径。使用广度优先,因为你要找的路径在根附近。

挑战2的解决方案

在深度优先搜索一章中,你学到了如何迭代实现该算法的方法。让我们看看如何递归地实现它。

extension Graph where Element: Hashable {

  func depthFirstSearch(from start: Vertex<Element>)
                        -> [Vertex<Element>] {
    var visited: [Vertex<Element>] = [] // 1
    var pushed: Set<Vertex<Element>> = [] // 2
    depthFirstSearch(from: start, // 3
                     visited: &visited,
                     pushed: &pushed)
    return visited
  }
}
  1. visited按顺序记录所访问的顶点。
  2. pushed记录哪些顶点被访问过。
  3. 通过调用一个辅助函数递归地执行深度优先搜索。

这个辅助函数看起来像这样:

func depthFirstSearch(from source: Vertex<Element>,
                      visited: inout [Vertex<Element>],
                      pushed: inout Set<Vertex<Element>>) {
  pushed.insert(source) // 1
  visited.append(source)

  let neighbors = edges(from: source)
  for edge in neighbors { // 2
    if !pushed.contains(edge.destination) {
      depthFirstSearch(from: edge.destination, // 3
                       visited: &visited,
                       pushed: &pushed)
    }
  }
}
  1. source顶点插入队列,并将其标记为访问。
  2. 对于每条相邻的边。
  3. 只要相邻的顶点还没有被访问过,就继续递归地沿着分支深入下去。

总的来说,深度优先搜索的时间复杂度是O(V + E)

挑战3的解决方案

当边和顶点的路径回到同一源头时,图就有一个循环。

extension Graph where Element: Hashable {

  func hasCycle(from source: Vertex<Element>) -> Bool  {
    var pushed: Set<Vertex<Element>> = [] // 1
    return hasCycle(from: source, pushed: &pushed) // 2
  }
}
  1. pushed是用来记录所有被访问的顶点。
  2. 通过调用一个辅助函数,递归地检查图中是否有一个循环。

这个辅助函数看起来像这样:

func hasCycle(from source: Vertex<Element>,
              pushed: inout Set<Vertex<Element>>) -> Bool {
  pushed.insert(source) // 1

  let neighbors = edges(from: source) // 2
  for edge in neighbors {
    if !pushed.contains(edge.destination) &&
       hasCycle(from: edge.destination, pushed: &pushed) { // 3
      return true
    } else if pushed.contains(edge.destination) { // 4
      return true
    }
  }
  pushed.remove(source) // 5
  return false // 6
}
  1. 为了启动该算法,首先插入源顶点。
  2. 对于每条相邻的边。
  3. 如果相邻顶点之前没有被访问过,则递归地深入到一个分支中,检查是否有循环。
  4. 如果相邻的顶点以前被访问过,那么你就找到了一个循环。
  5. 移除source顶点,这样你就可以继续寻找其他有潜在循环的路径。
  6. 没有找到循环。

你本质上是在进行深度优先的图形遍历,通过递归潜入一条路径直到找到一个周期,然后通过弹出堆栈来回溯找到另一条路径。时间复杂度是O(V+E)