lib/rgl/traversal.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# traversal.rb
#
require 'rgl/base'
require 'rgl/graph_visitor'
require 'rgl/graph_iterator'

module RGL

  # File +traversal.rb+ defines the basic graph traversal algorithm for DFS and
  # BFS search. They are implemented as a {GraphIterator}, which is a +Stream+
  # of vertices of a given graph. The streams are not reversable.
  #
  # Beside being an iterator in the sense of the Stream mixin, {BFSIterator} and
  # {DFSIterator} follow the
  # {https://www.boost.org/libs/graph/doc/visitor_concepts.html BGL Visitor
  # Concepts} in a slightly modified fashion (especially for the
  # {DFSIterator}).
  #
  # A {BFSIterator} can be used to traverse a graph from a given start vertex in
  # breath first search order. Since the iterator also mixins the {GraphVisitor},
  # it provides all event points defined there.
  #
  # The vertices which are not yet visited are held in the queue +@waiting+.
  # During the traversal, vertices are *colored* using the colors :GRAY
  # (when waiting) and :BLACK when finished. All other vertices are :WHITE.
  #
  # For more see the BGL {BFS Visitor
  # Concept}[https://www.boost.org/libs/graph/doc/BFSVisitor.html].
  #
  # See the implementation of {Graph#bfs_search_tree_from} for an example usage.
  #
  # @see Graph#bfs_iterator
  # @see Graph#bfs_search_tree_from
  class BFSIterator

    include GraphIterator
    include GraphVisitor

    # @return the vertex where the search starts
    attr_accessor :start_vertex

    # Create a new {BFSIterator} on _graph_, starting at vertex _start_.
    #
    def initialize(graph, start = graph.detect { |x| true })
      super(graph)
      @start_vertex = start
      set_to_begin
    end

    # Returns true if +@color_map+ has only one entry (for the start vertex).
    #
    def at_beginning?
      @color_map.size == 1
    end

    # Returns true if @waiting is empty.
    #
    def at_end?
      @waiting.empty?
    end

    # Reset the iterator to the initial state (i.e. at_beginning? == true).
    #
    def set_to_begin
      # Reset color_map
      @color_map = Hash.new(:WHITE)
      color_map[@start_vertex] = :GRAY
      @waiting = [@start_vertex]           # a queue
      handle_tree_edge(nil, @start_vertex) # discovers start vertex
      self
    end

    # @private
    def basic_forward
      u = next_vertex
      handle_examine_vertex(u)

      graph.each_adjacent(u) do |v|
        handle_examine_edge(u, v)
        if follow_edge?(u, v) # (u,v) is a tree edge
          handle_tree_edge(u, v) # also discovers v
          color_map[v] = :GRAY   # color of v was :WHITE
          @waiting.push(v)
        else # (u,v) is a non tree edge
          if color_map[v] == :GRAY
            handle_back_edge(u, v) # (u,v) has gray target
          else
            handle_forward_edge(u, v) # (u,v) has black target
          end
        end
      end

      color_map[u] = :BLACK
      handle_finish_vertex(u) # finish vertex
      u
    end

    protected

    def next_vertex
      # waiting is a queue
      @waiting.shift
    end

  end # class BFSIterator


  module Graph

    # @return [BFSIterator] starting at vertex _v_.
    def bfs_iterator(v = self.detect { |x| true })
      BFSIterator.new(self, v)
    end

    # This method uses the +tree_edge_event+ of BFSIterator
    # to record all tree edges of the search tree in the result.
    #
    # @return [DirectedAdjacencyGraph] which represents a BFS search tree starting at _v_.
    def bfs_search_tree_from(v)
      unless defined?(DirectedAdjacencyGraph)
        require 'rgl/adjacency'
      end
      bfs  = bfs_iterator(v)
      tree = DirectedAdjacencyGraph.new

      bfs.set_tree_edge_event_handler do |from, to|
        tree.add_edge(from, to)
      end

      bfs.set_to_end # does the search
      tree
    end

  end # module Graph

  # Iterator for a depth first search, starting at a given vertex. The only
  # difference from {BFSIterator} is that +@waiting+ is a stack, instead of a queue.
  #
  # Note that this is different from {DFSVisitor}, which is used in the recursive
  # version for depth first search (see {Graph#depth_first_search}).
  #
  # @see Graph#dfs_iterator
  class DFSIterator < BFSIterator

    def next_vertex
      # waiting is a stack
      @waiting.pop
    end

  end # class DFSIterator

  # A DFSVisitor is needed by the {Graph#depth_first_search} and
  # {Graph#depth_first_visit} methods of a graph. Besides the eventpoints of
  # {GraphVisitor}, it provides an additional eventpoint +start_vertex+, which is
  # called when a +depth_first_search+ starts a new subtree of the depth first
  # forest that is defined by the search.
  #
  # @see DFSIterator
  class DFSVisitor

    include GraphVisitor

    def_event_handler 'start_vertex'

  end # class DFSVisitor


  module Graph

    # @return [DFSIterator] staring at vertex _v_.
    def dfs_iterator(v = self.detect { |x| true })
      DFSIterator.new(self, v)
    end

    # Do a recursive DFS search on the whole graph. If a block is passed, it is
    # called on each +finish_vertex+ event. See {Graph#strongly_connected_components}
    # for an example usage.
    #
    # Note that this traversal does not garantee, that roots are at the top of
    # each spanning subtree induced by the DFS search on a directed graph (see
    # also the discussion in issue #20[https://github.com/monora/rgl/issues/20]).
    # @see DFSVisitor
    def depth_first_search(vis = DFSVisitor.new(self), &b)
      each_vertex do |u|
        unless vis.finished_vertex?(u)
          vis.handle_start_vertex(u)
          depth_first_visit(u, vis, &b)
        end
      end
    end

    # Start a depth first search at vertex _u_. The block _b_ is called on
    # each +finish_vertex+ event.
    # @see DFSVisitor
    def depth_first_visit(u, vis = DFSVisitor.new(self), &b)
      vis.color_map[u] = :GRAY
      vis.handle_examine_vertex(u)

      each_adjacent(u) do |v|
        vis.handle_examine_edge(u, v)

        if vis.follow_edge?(u, v)          # (u,v) is a tree edge
          vis.handle_tree_edge(u, v)       # also discovers v
          # color of v was :WHITE. Will be marked :GRAY in recursion
          depth_first_visit(v, vis, &b)
        else                               # (u,v) is a non tree edge
          if vis.color_map[v] == :GRAY
            vis.handle_back_edge(u, v)     # (u,v) has gray target
          else
            vis.handle_forward_edge(u, v)  # (u,v) is a cross or forward edge
          end
        end
      end

      vis.color_map[u] = :BLACK
      vis.handle_finish_vertex(u) # finish vertex
      b.call(u)
    end

  end # module Graph

end # module RGL