Version 09/03/2018 Notes Index ↑ 

Graphs 1: Representation, Search, and Survey

1 Definitions, Notation, and Representations

Graphs and directed graphs are studied extensively in the pre-requisit math sequence Discrete Mathematics I [FSU::MAD2104], so these notes will not dwell on details. We will however mention key concepts in order to establish notation, especially where there may be slight variations in terminology. Generally we denote a graph (directed or undirected) by G =(V,E).

1.1 Undirected Graphs - Theory and Terminology

  • Undirected Graph: aka Ungraph, Bidirectional Graph, Bigraph, Graph
  • V = {vertices}, |V| = the number of vertices of G
  • E = {edges}, |E| = the number of edges of G
  • Adjacency: vertices x and y are said to be adjacent iff [x,y] ∈ E.
  • Assume graphs are simple: at most one edge between two vertices, and no loops (i.e., no edge from a vertex to itself). Note that the notation [x,y] means an undirected edge, denoted by {x,y} in the MAD2104 notes.
  • Path from v to w: a set {x0,...,xk} of vertices such that x0 = v, xk = w, and [xi-1,xi] ∈ E for i = 1,...,k. The edges [xi-1,xi] are called the edges of the path and k is the length of the path. A path is simple if the vertices defining it are distinct - that is, not repeated anywhere along the path. If P is a path from v to w we say w is reachable from v. Denote the set of edges joining the vertices of a path P by E(P) = { [xi-1,xi] | i = 1,...,k }. Note: If P is a path from v to w, the pair (P, E(P)) is a subgraph of G. It is common practice to call P (the vertices), E(P) (the edges), and (P, E(P)) (the subgraph) "the path", depending on context.
  • Connected Graph: for all vertices v,wV, w is reachable from v.
  • Cycle: A path of length at least 3 from a vertex to itself.
  • Acyclic Graph: Graph with no cycles
  • Degree of a vertex v: deg(v) = the number of edges with one end at v.

Theorem 1u. In an undirected graph, ∑vV deg(v) = 2 × |E|.

1.2 Directed Graphs - Theory and Terminology

  • Directed Graph: aka Digraph
  • V = {vertices}, |V| = the number of vertices of G
  • E = {edges}, |E| = the number of edges of G
  • Adjacency: Vertex x is adjacent to vertex y, and y is adjacent from x, iff (x,y) ∈ E.
  • Note that an edge from x to y has a direction. Thus in a directed graph there is the possibility of an opposite edge from y to x. The notation for a directed edge is the ordered pair (x,y), the same notation used in the MAD2104 notes. In addition to allowing directed edges in both directions, we often allow directed self-edges (directed loops) in a digraph.
  • Path from v to w: a set {x0,...,xk} of vertices such that x0 = v, xk = w, and (xi-1,xi) ∈ E for i = 1,...,k. The directed edges (xi-1,xi) are called the edges of the path and k is the length of the path. A path is simple if the vertices defining it are distinct - that is, not repeated anywhere along the path. If P is a path from v to w we say w is reachable from v. Denote the set of directed edges joining the vertices of a path P by E(P) = { (xi-1,xi) | i = 1,...,k }. Note: If P is a path from v to w, the pair (P, E(P)) is a subgraph of G. It is common practice to call P (the vertices), E(P) (the edges), and (P, E(P)) (the subgraph) "the path", depending on context. Note: the edges of a path in a digraph are all directed and must orient in the forward direction.
  • Strongly Connected Digraph: for all vertices v,wV, w is reachable from v. Note this means the existence of two directed paths - one from v to w and the other from w to v. Also note that putting the two paths together creates a (directed) cycle containing both v and w.
  • (Directed) Cycle: A path of length at least 1 from a vertex to itself. Note this is distinct from the undirected case, allowing paths of length 1 (so-called self-loops) and length 2 (essentially an out-and-back path, equivalent to a two-way edge).
  • Directed Acyclic Graph [DAG]: Digraph with no directed cycles
  • InDegree of a vertex v: inDeg(v) = the number of edges to v.
  • OutDegree of a vertex v: outDeg(v) = the number of edges from v.

There are subtle distinctions in the way graphs and digraphs are treated in definitions. There are also "conversions" from graph to digraph and digraph to graph.

  • Undirected graph edges have no preferred direction and are represented using the notation [x,y] to mean the un-ordered pair in which [x,y] = [y,x]. Directed graph edges have a preferred direction and are represented using the notation (x,y) to mean the ordered pair in which (x,y) ≠ (y,x) when xy.
  • In a directed graph, vertex y is said to be a neighbor of the vertex x iff (x,y) ∈ E. In an undirected graph, being neighbors and being adjacent are equivalent. In digraphs, y is a neighbor of x iff y is adjacent from x.
  • If D = (V,E) is a digraph, the undirected version of D is the graph D' = (V,E')$ where [x,y] ∈ E' iff xy and (x,y) ∈ E. Note this replaces, in effect, a 1-way edge with a 2-way edge (or two opposite 1-way edges in the representations), except self-loops are eliminated.
  • If G = (V,E) is a graph, the directed version of G is the graph G' = (V,E') where (x,y) ∈ E' iff [x,y] ∈ E. Note this replaces each undirected edge [x,y] with two opposing directed edges (x,y) and (y,x).

Theorem 1d. In a directed graph, ∑vV inDeg(v) = ∑vV outDeg(v) = |E|.

The proof of Theorem 1 uses aggregate analysis. First show the result for directed graphs, where edges are in 1-1 correspondence with their initiating vertices. Then apply 1d to the directed version of an undirected graph and adjust for the double counting. ∎

1.3 Graph and Digraph Representations

There are two fundamental families of representation methods for graphs - connectivity matrices and adjacency sets. Two archetypical examples apply to graphs and digraphs G = (V,E) with the assumption that the vertices are numbered starting with 0 (in C fashion): V = {v0, v1 , v2, ... ,vn-1}.

1.3.1 Adjacency Matrix Representation

In the adjacency matrix representation we define an n × n matrix M by

M(i,j) = (G has an edge from vi to vj ? 1 : 0)

M is the adjacency matrix representation of G.

For example, consider the graph G1 depicted here:

              0 --- 1     2 --- 6 --- 7
Graph G1      |           |           |
              |           |           |
              3 --- 4 --- 5 --- 8 --- 9

The adjacency matrix for G1 is:

- 1 - 1 - - - - - -
1 - - - - - - - - -
- - - - - 1 1 - - -
1 - - - 1 - - - - -
- - - 1 - 1 - - - -
- - 1 - 1 - - - 1 -
- - 1 - - - - 1 - -
- - - - - - 1 - - 1
- - - - - 1 - - - 1
- - - - - - - 1 1 -

(where we have used "-" instead of "0" for readability). Adjacency matrix representations are very handy for some graph contexts. They are easy to define, low in overhead, and there is not a lot of data structure programming involved. And some questions have very quick answers:

  1. Using an adjacency matrix representation, the question "Is there an edge from vi to vj ?" is answered in constant time by interpreting M(i,j) as boolean.

Adjacency matrix representations suffer some disadvantages as well, related to the fact that an n × n matrix has n2 places to store and visit:

  1. An adjacency matrix representation requires Θ(n2) storage.
  2. A loop touching all edges of a graph (or digraph) represented with an adjacency matrix requires at least Ω(n2) time.

Particularly when graphs are sparse - a somewhat loose term implying that the number of edges is much smaller than it might be - these basic storage and traversal tasks should not be so expensive.

1.3.2 Sparse Graphs

A graph (or digraph) could in principle have an edge from any vertex to any other. (We don't allow self-edges in undirected graphs without specifically warning the context that self-edges are allowed.) In other words the adjacency matrix representation might have 1's almost anywhere, out of a total of n2 possibilities. In practice, most large graphs encountered in real life are quite sparse, with vertex degree significantly limited, so that the number of edges is O(n) and the degree of each vertex may even be bounded by a constant. (For example, Euler's Formula on planar graphs implies that if G is planar then |E| ≤ 3|V| - 6, hence |E| = O(|V|).) Observe:

  1. Suppose d is an upper bound on the degree of the vertices of a graph (or digraph). Then d × n is an upper bound on the number of edges in the graph.

So, a graph with, say, 10,000 vertices, and vertex degree bounded by 100, would have no more than 1,000,000 edges, whereas the adjacency matrix would have storage for 100,000,000, with 99,000,000 of these representing "no edge".

The human brain can be modeled loosely as a directed graph with vertices representing neuronal cells and edges representing direct communication from one neuron to another. This digraph is estimated to have about 1010 vertices with average degree about 103. An adjacency matrix for this model has 1010×2 = 1020 entries with at most 1013 "1" entries, thus wasting at least

999,999,999,000,000,000,000

units of memory.

1.3.3 Adjacency List Representation

The adjacency list representation is designed to require storage to represent edges only when they actually exist in the graph. There is per-edge overhead required, but for sparse graphs this overhead is negligible compared to the space used by adjacency matrices to store "no info".

The adjacency list uses a vector v of lists reminiscent of the supporting structure for hash tables. The vector is indexed in the range [0..n), and the list v[i] consists of the subscripts of vertices that are adjacent from vertex vi.

On the other hand, unlike the situation with hash tables where we could choose the size of the vector to maintain the average list size to approximately 1, we have no such luxury in representing graphs. The index range for the vector represents exactly the vertices of the graph.

For example, the adjacency list representation of the graph G1 illustrated above is:

v[0]: 1 , 3
v[1]: 0                   0 --- 1     2 --- 6 --- 7
v[2]: 5 , 6               |           |           |
v[3]: 0 , 4               |           |           |
v[4]: 3 , 5               3 --- 4 --- 5 --- 8 --- 9
v[5]: 2 , 4 , 8                   Graph G1
v[6]: 2 , 7
v[7]: 6 , 9
v[8]: 5 , 9
v[9]: 7 , 8

As with the matrix representation, time and space requirements for adjacency list representations are based on what needs to be traversed and stored:

  1. An adjacency list representation requires Θ(|V| + |E|) storage
  2. A loop touching all edges of a graph (or digraph) represented with an adjacency list representation requires at least Ω(|V| + |E|) time.

Proofs of the last two factoids use aggregate analysis.

Note that when G has many edges, for example when |E| = Θ(|V|2), the estimates are the same as for matrix representations. But for sparse graphs, in particular when |E| = O(|V|), the estimates are dramatically better - linear in the number of vertices.

1.3.4 Undirected v. Directed Graphs

Both the adjacency matrix and adjacency list can represent either directed or undirected graphs. And in both systems, an undirected edge has a somewhat redundant representation.

An adjacency matrix M represents an undirected graph when it is symmetric about the main diagonal:

M(i,j) = M(j,i) for all i,j

One could think of this constraint as a test whether the underlying graph is directed or not.

An adjacency list v[] represents an undirected graph when all edges (x,y) appear twice in the lists - once making y adjacent from x and once making x adjacent from y:

...
v[x]: ... , y , ...
...
v[y]: ... , x , ...
...

1.3.5 Variations on Adjacency Lists

Many graph algorithms require examining all vertices adjacent from a given vertex. For such algorithms, traversals of the adjacency lists will be required. These traversals are inherently Ω(list.size), and replacing list with a faster access time set structure might even slow down the traversal (although not affecting its asymptotic runtime).

If a graph process requires many direct queries of the form "is y adjacent from x" (as distinct from traversing the entire adjacency list), a question that requires Ω(d) time to answer using sequential search in a list of size d, it can be advantageous to replace list with a faster access set structure, which would allow the question to be answered in time O(log d). (In the graph context, d is the size of the adjacency list and the outDegree of the vertex whose adjacency list is being searched.)

1.3.6 Dealing with Graph Data

Often applications require maintaining data associated with vertices and/or edges. Vertex data is easily maintained using auxilliary vectors. (An associative array can be used to map vertex data back to vertex number.) Edge data in an adjacency matrix representation is also straightforward to maintain in another matrix.

Edge data is slightly more difficuly to maintain in an adjacency list representation, in essence because the edges are only implied by the representation. This problem can be handled in two ways. Adjacency lists can be replaced with edge lists - instead of listing adjacent vertices, list pointers to the edges that connect the adjacent pairs. Then an edge can be as elaborate as needed. It must at minimum know what its two vertex ends are, of course, something like this:

template <class T>
struct Edge
{
  T data_;              // whatever needs to be stored  
  unsigned from_, to_;  // vertices of edge
};

Or a hash table can be set up to store edge data based on keys of the form (x,y), where (x,y) is the edge implied by finding y in the list v[x].

1.4 A Possible Graph Framework

We now have four distinct representations that might be useful in actual code: adjacency matrix and adjacency list representations for both undirected and directed graphs. The following class hierarchy provides a roadmap for defining a framework providing all four representations, the advantages being enforcement of terminology uniformity and re-using code by elevating to a parent class where appropriate. Even though we will not implement this entire framework, the pseudo-code listings are important to understand as they provide fairly precise definitions of how graph classes can be coded.

Graph                  // abstract base class
  AdjacencyMatrixBase  // base class for adj matrix reps
    AMUGraph           // adj matrix representation, undirected graph
    AMDGraph           // adj matrix representation, directed graph
  AdjacencyListBase    // base class for adj list reps
    ALUGraph           // adj list representation, undirected graph
    ALDGraph           // adj list representation, directed graph

It may seem odd to violate the pattern of not signaling the specific implementation of an ADT to the client programmers. However, it is appropriate in the case of graphs because space usage and many algorithms have widely disparate asymptotic time and space behavior that depends specifically on representation.

The following pseudo-code provides detail to the roadmap. The methods are sufficient to support many graph models and algorithms:

class Graph  // abstract base class for all representations
{
  typedef unsigned Vertex;
public:
  virtual void     SetVrtxSize (unsigned n) = 0;
  virtual void     AddEdge     (Vertex from, Vertex to) = 0;
  virtual bool     HasEdge     ()           const;
  virtual unsigned VrtxSize    ()           const;
  virtual unsigned EdgeSize    ()           const;
  virtual unsigned OutDegree   (Vertex v)   const = 0;
  virtual unsigned InDegree    (Vertex v)   const = 0;
  ...
};

class AdjacencyMatrixBase : public Graph
{
...
  AdjIterator   Begin (Vertex x) const;
  AdjIterator   End   (Vertex x) const;
...
  fsu::Matrix am_;
};

class AdjacencyListBase   : public Graph
{
...
  AdjIterator   Begin (Vertex x) const;
  AdjIterator   End   (Vertex x) const;
...
  fsu::Vector < fsu::List < Vertex > > al_;
};

The AdjIterator type needs to be defined for both matrix and list representations. AdjIterator must be at least a forward ConstIterator traversing the collection of vertices adjacent from v. For the adjacency list representation AdjIterator is best defined as a list ConstIterator. Begin(x) can be defined as follows:

AdjIterator AdjacncyListBase::Begin (Vertex x)
{
  return al_[x].Begin();
}

(For the adjacency matrix representation, AdjIterator is defined in terms of a vector iterator on rows of the matrix, skipping over zero entries of the matrix and stopping at one entries which represent extant edges. This makes a traversal of the neighbors of v a Θ(|V|) process, very inefficient for a sparse graph. We will not delve further into the adjacency matrix AdjIterator.)

The following implementations completely clarify how edges are represented in all four situations:

AMDGraph::AddEdge(Vertex x, Vertex y)
{
  am_(x,y) = 1;
}

AMUGraph::AddEdge(Vertex x, Vertex y)
{
  am_(x,y) = 1;
  am_(y,x) = 1;
}

ALDGraph::AddEdge(Vertex x, Vertex y)
{
  al_[x].Insert(y);
}

ALUGraph::AddEdge(Vertex x, Vertex y)
{
  al_[x].Insert(y);
  al_[y].Insert(x);
}

Note that it is impossible to add a redundant edge using the adjacency matrix representation, but quite possible in the adjacency list representation. The AddEdge method could be upgraded to check for an existing edge before adding one, but that would require a (sequential) search of the list and make AddEdge runtime linear in the size of the neighbor list instead of the constant time as is. If the adjacency list is replaced with a unimodal set type, this problem would go away. Generally, we just leave it to client programs to correctly manage their graphs.

A final recommendation on the graph framework is to use class templates. The advantage is that template code is not compiled unless it is actually used in code, so template-izing the classes results in leaner code for applications - compiling only what the client needs. The unsigned type used for vertex representation makes a convenient template parameter.

1.5 Actual Code

While the construction of a Graph framework as outlined is an interesting and useful software development project, we will not build the entire framework for LIB. We concentrate on adjacency list representations and therefore get by with two classes: a base class for undirected graphs and a derived class for directed graphs.

template < typename N >
class ALUGraph
{
public:
  typedef N                                      Vertex;
  typedef typename fsu::List<Vertex>             SetType;
  typedef typename SetType::ConstIterator        AdjIterator;

  void   SetVrtxSize  (N n);
  size_t VrtxSize     () const;
  void   AddEdge      (Vertex from, Vertex to);
  bool   HasEdge      (Vertex from, Vertex to) const;
  size_t EdgeSize     () const;           // Theta (|V| + |E|)
  size_t OutDegree    (Vertex v) const;
  size_t InDegree     (Vertex v) const;

  void Clear          ();
  void Dump           (std::ostream& os); // Theta (|V| + |E|)
    
  AdjIterator Begin   (Vertex x) const;
  AdjIterator End     (Vertex x) const;

  ALUGraph            ();
  ALUGraph            (N n);

protected:
  fsu::Vector < SetType > al_;
};

There is no new data in the derived class, and the base class iterator support methods work for the derived class, because the definition of iterator is unchanged. Only three methods need to be re-defined/over-ridden for the directed case:

template < typename N >
class ALDGraph : public ALUGraph <N>
{
public:
  typedef N                                          Vertex;
  typedef typename ALUGraph<N>::SetType              SetType;
  typedef typename ALUGraph<N>::AdjIterator          AdjIterator;

  // void   SetVrtxSize  (N n);
  // size_t VrtxSize     () const;
  void   AddEdge      (Vertex from, Vertex to);
  // bool   HasEdge      (Vertex from, Vertex to) const;
  size_t EdgeSize     () const;          // Theta (|V| + |E|)
  // size_t OutDegree    (Vertex v) const;
  size_t InDegree     (Vertex v) const;  // Theta (|V| + |E|)

  // void Clear          ();
  // void Dump           (std::ostream& os);  // Theta (|V| + |E|)

  // AdjIterator Begin   (Vertex x) const;
  // AdjIterator End     (Vertex x) const;

  ALDGraph               ();
  ALDGraph               ( N n );

  // new method - creates d as the reverse directed graph of *this 
  void Reverse(ALDGraph& d) const;
};

Implementations are mostly very straightforward based on the discussions above. We present several here to illustrate the use of the adjacency list and AdjIterators.

template < typename N >
bool ALUGraph<N>::HasEdge (Vertex from, Vertex to) const
{
  AdjIterator i = al_[from].Includes(to);
  if (i == End(from))
    return 0;
  return 1;
}

template < typename N >
size_t ALUGraph<N>::EdgeSize () const // Theta (|V| + |E|)
// Theta (|V| + |E|)
{
  size_t esize = 0;
  for (Vertex v = 0; v < al_.Size(); ++v)
    esize += al_[v].Size();
  return esize/2;
}

template < typename N >
void ALUGraph<N>::Dump (std::ostream& os)
// Theta (|V| + |E|)
{
  AdjIterator j;
  for (Vertex v = 0; v < VrtxSize(); ++v)
  {
    os << '[' << v << "]->";
    j = this->Begin(v);
    if (j != this->End(v))
    {
      os << *j;
      ++j;
    }
    for ( ; j != this->End(v); ++j)
    {
      os << ',' << *j;
    }
    os << '\n';
  }
}

template < typename N >
size_t ALDGraph<N>::EdgeSize () const
// Theta (|V| + |E|)
{
  size_t esize = 0;
  for (Vertex v = 0; v < ALUGraph<N>::al_.Size(); ++v)
    esize += ALUGraph<N>::al_[v].Size();
  return esize;
}

template < typename N >
size_t ALDGraph<N>::InDegree (Vertex v) const
// Theta (|V| + |E|)
{
  size_t indegree = 0;
  AdjIterator j;
  for (Vertex x = 0; x < ALUGraph<N>::VrtxSize(); ++x)
  {
    for (j = this->Begin(x); j != this->End(x); ++j)
    {
      if (v == *j) ++indegree;
    }
  }
  return indegree;
}

1.6 Exercises

  1. Create the classes ALUGraph and ALDGraph in the file graph.h. The classes should be in the fsu namespace.
    Instructions: Do not copy/paste from these notes and do not copy code from another source. TYPE all of the code using a simple text editor, and make sure that you understand what you are typing. Every token should come out of your brain through your fingers into a text file.

  2. Locate the source code file fgraph.cpp and compile in the directory with graph.h. Debug graph.h as necessary.

  3. Create a file named "graph1.10.10" with exactly this content:

    #
    # file graph1.10.10
    #
    # This is the graph G1                   0 --- 1     2 --- 6 --- 7
    # depicted to the right                  |           |           |
    #                                        |           |           |
    # G1 has 10 vertices                     3 --- 4 --- 5 --- 8 --- 9 
    # G1 has 10 edges                                Graph G1
    # 
    # G1 is used in the notes chapter on graphs
    #
    
    10
    0 1 0 3
    2 5 2 6
    3 4
    4 5
    5 8
    6 7
    7 9
    8 9
    

    It is OK to copy/paste for this file. Note that this is a graph data file that represents the graph used in the illustration in this section.

  4. Create several more graph data files using the spec:

     -----------------------------------------------------------------------
     Graph File Specs                                                       
     ----------------                                                       
     1 File documentation at beginning of file, lines begin with '#'        
     2 Remainder of file consists of unsigned integers in decimal notation  
     2.1 First number is vertex size                                        
     2.2 Remaining numbers occur in pairs                                   
     2.3 Pair represents an edge "from" "to"                                
     2.4 Edge vertices must be in range determined by 2.1                   
     -----------------------------------------------------------------------
    

    and run fgraph.x on these graph data files. Be sure you agree with the results.

  5. Locate the executable file rangraph.x. This application creates random graph data files with a specified vertex and edge size. Use rangraph.x to create some large graph data files, and make sure your graph classes handle large graphs by running fgraph.x on them.

  6. Locate the executable file agraph.x. This application does an analysis of graphs specified by a graph data file. Run on all of your graph data files. Read and understand the results. For the small graphs, verify the results by hand.

2 Breadth First Search

One of the first things we want to do in a graph or digraph is find our way around. There are two widely used and famous processes to perform a search in a graph, both of which have been introduced and used in other contexts: depth-first and breadth-first search. In trees, for example, preorder and postorder traversals follow the depth-first search process, and levelorder traversal follows the breadth-first process. And solutions to maze problems typically use one or the other to construct a solution path from start to goal. Trees and mazes are representable as special kinds of graphs (or digraphs). Mazes, in particular, provide an excellent context to study these fundamental algorithms.

We will assume throughout the remainder of this chapter that G=(V,E) is a graph, directed or undirected, with |V| vertices and |E| edges. We also assume that the graph is presented to the search algorithms using the adjacency list representation.

2.1 BFSearch(v)

The Breadth-First Search [BFS] process begins at a vertex of G and explores the graph from that vertex. At any stage in the search, BFS considers all vertices adjacent from the current vertex before proceeding deeper into the graph. The process is a direct "upgrade" to the Levelorder Traversal of a tree, using a control queue conQ_. The name of the graph is g_:

BFSearch( Vertex v )
{
  conQ_.Push(v);
  mark v visited
  while (!conQ_.Empty())
  {
    // add all unvisited neighbors n of the front to the queue; n = *i
    Vertex front = conQ_.Front();
    for ( AdjIterator i = g_.Begin(front); i != g_.End(front); ++i )
    {
      if (*i is not visited)
      {
        conQ_.Push(*i);
        mark *i visited
      }
    }
    // remove front of queue
    conQ_.Pop();
  }
}

This pseudocode correctly describes the BFS algorithm, but a couple of things need to be clarified:

  1. How do we extract a solution path from v to another vertex x ?
  2. How do we implement "mark a vertex visited" ?

Re 1: Maintain an array of vertex numbers "parent_" so that parent_[v] is the vertex from which v was discovered. Then a path from v to x is the reverse of sequence of vertices
     (x, parent_[x], parent_[parent_[x]], ..., v). (In a directed graph context, the path is directional from v to x.)

Re 2: One possibility is to maintain a bool array "visited_". However we want to keep track of another property with 3 possible values: (a) undiscovered, (b) discovered and being processed (that is, still in the control queue), and (c) processing completed (that is, already removed from the control queue). Property (a) is equivalent to "unvisited", so we have access to that information.

We follow the exposition in [Cormen et al (2009), Introduction to Algorithms (3rd ed.), MIT Press, Cambridge, MA] and use 3 colors: white for unvisited vertices, grey for vertices in the control queue, and black for vertices that have already cycled through the control queue. Colors are maintained in the array color_. The following provides the remaining details for BFS Search(v):

Search( Vertex v )
{
  conQ_.Push(v);
  color_[v] = grey;
  while (!conQ_.Empty())
  {
    Vertex front = conQ_.Front();
    // Push all unvisited neighbors of the front
    for ( AdjIterator i = g_.Begin(front); i != g_.End(front); ++i )
    {
      if (color_[*i] == white) // unvisited
      {
        parent_[*i] = front;
        color_[*i] = grey;
        conQ_.Push(*i);
      }
    }
    // Pop the queue
    conQ_.Pop();
    color_[front] = black;
  }
}

The runtime of BFSearch(v) is straightforward to estimate. Note that a vertex only changes color from white to grey to black, so each vertex is processed at most one time in the outer loop (one push and one pop), for a total cost of 2×|V|. Note also that the for loop traverses the adjacency list of a vertex only one time, when that vertex is at the front of the queue, so that the cummulative cost of all the for loop executions is

vOutDegree(v) ≤ 2×|E|

(using Theorem 1 to get the 2×|E| bound). Thus the total cost of BFSearch(v) is

2×|V| + 2×|E| ≤ O(|V| + |E|)

If every vertex is processed, the result is exactly Θ(|V| + |E|). These are as efficient as possible, because just touching all the vertices and edges has the same cost estimate. These observations are summarized in the theorems at the end of this section.

As an example, performing BFSearch(5) on the graph G1 encounters the vertices as follows:

adj list rep       Graph G1                       BFS::conQ
------------       --------                       <--------
v[0]: 1 , 3                                       null         ...
v[1]: 0            0 --- 1     2 --- 6 --- 7      5            6 3 9
v[2]: 5 , 6        |           |           |      5 2          6 3 9 7
v[3]: 0 , 4        |           |           |      5 2 4        3 9 7
v[4]: 3 , 5        3 --- 4 --- 5 --- 8 --- 9      5 2 4 8      3 9 7 0
v[5]: 2 , 4 , 8                                   2 4 8        9 7 0
v[6]: 2 , 7                                       2 4 8 6      7 0
v[7]: 6 , 9                                       4 8 6        0
v[8]: 5 , 9                                       4 8 6 3      0 1
v[9]: 7 , 8                                       8 6 3        1
                                                  8 6 3 9      null
                                                  ...
Vertex discovery order: 5 2 4 8 6 3 9 7 0 1
   grouped by distance: [ (5) (2 4 8) (6 3 9) (7 0) (1) ]

Another set of information illustrated above is distance from the start vertex. We will show later in these notes that the paths calculated by BFSearch(v) are shortest possible paths, so that the length of the path from x back to v is by definition the distance from x to v.

Finally, we note that the BFSearch algorithm works equally well for undirected and directed graphs.

2.2 BFSurvey

We have got to the point that our BFS algorithm is most efficiently discussed and used as a class with its own data. We keep track of both distance and time, along with color and parent info.

template < class G >
class BFSurvey
{
public:
  typedef G                           Graph;
  typedef typename Graph::Vertex      Vertex;
  typedef typename Graph::AdjIterator AdjIterator;

  BFSurvey    ( const Graph& g );
  BFSurvey    ( const Graph& g , Vertex start );
  void Search ( );
  void Search ( Vertex v );
  void Reset  ( );
  void Reset  ( Vertex start );

  fsu::Vector < Vertex >        distance_;  // distance from search origin
  fsu::Vector < Vertex >        dtime_;     // discovery time
  fsu::Vector < Vertex >        parent_;    // for BFS tree
  fsu::Vector < char >          color_;     // using chars 'w'=white, 'g'=grey, 'b'=black

private:
  const Graph&                  g_;
  Vertex                        start_;     // default is vertex 0
  size_t                        time_;      // global sequencing clock
  size_t                        infinity_;  // unreachable distance = 1+|E|
  size_t                        forever_;   // unreachable time = |V|
  Vertex                        null_;      // undefined vertex = |V|
  fsu::Deque  < Vertex >        conQ_;      // control queue
};

The template parameter is intended to be a graph type (ALUGraph or ALDgraph). The const reference g_ must be set by the class constructors. The algorithm thus attaches itself to the graph, remura-like. It does not make a copy of the graph, it operates directly on the graph through this reference.

The notions of time and distance require some explanation. The distance distance_[x] represents the length of the "parent path" from a vertex x back to the beginning search vertex v. The longest conceivable path in a graph contains all of the edges, and therefore one more than the number of edges is an un-attainable distance in the graph. This allows us to define "infinity":

infinity_ = 1 + g_.EdgeSize() // impossibly long distance.

Time is kept by the "clock" class variable time_, initialized to zero by constructors and Reset methods. In order to ensure that each search event happens at its own unique time, we adopt the convention that time is always assigned using the postfix increment of the clock variable:

dtime[x] = time_++;

Because in BFS we assign a time only when a vertex is discovered (dtime_[x] is the discovery time of x) the sets V = { vertices } and AssignedTimes = { 0 1 2 ... last_time_assigned } are in 1-1 correspondence. Thus last_time_assigned is 1 less than the number of vertices, and the first un-assignable time is the number of vertices:

forever_ = g_.VrtxSize() // impossibly long time.

These values are instantiated by constructors and Reset methods.

Caution: Time is assigned twice to each vertex in depth-first search, so the notion of forever is 2*g_.VrtxSize() in a DFS setting.

2.3 Implementation of BFSurvey

Other than setting the graph reference, done only by constructors, constructors and Reset methods accomplish much the same thing:

  • Set the start vertex
  • Define "infinity" to be an impossibly long distance, 1 more than the number of edges
  • Define "forever" to be an impossibly long time, the number of vertices
  • Define "null" to be a non-existing vertex (the number of vertices, which is 1 past the vertex range)
  • Initialize all the informational vectors
  • Set all colors to white
  • Clear the control queue

template < class G >
BFSurvey<G>::BFSurvey (const Graph& g)
  : g_(g), start_(0), time_(0),
    infinity_(1+g_.EdgeSize()), forever_((Vertex)g_.VrtxSize()), null_((Vertex)g_.VrtxSize()),
    distance_ (g_.VrtxSize(), infinity_),
    dtime_    (g_.VrtxSize(), forever_),
    parent_   (g_.VrtxSize(), null_),
    color_    (g_.VrtxSize(), 'w'),   // 'w' = white
    conQ_()
{}

template < class G >
BFSurvey<G>::BFSurvey (const Graph& g , size_t start)
  : g_(g), start_(start), time_(0),
    infinity_(1+g_.EdgeSize()), forever_(g_.VrtxSize()), null_((Vertex)g_.VrtxSize()),
    distance_ (g_.VrtxSize(), infinity_),
    dtime_    (g_.VrtxSize(), forever_),
    parent_   (g_.VrtxSize(), null_),
    color_    (g_.VrtxSize(), 'w'),   // 'w' = white
    conQ_()
{}

template < class G >
void BFSurvey<G>::Reset()
{
  time_ = 0;
  conQ_.Clear();
  if (color_.Size() != g_.VrtxSize()) // g_ has changed vertex size
  {
    infinity_ = 1+g_.EdgeSize();       // unreachable distance
    forever_  = g_.VrtxSize();         // unreachable time
    null_     = (Vertex)g_.VrtxSize(); // undefined parent
    distance_.SetSize (g_.VrtxSize(), infinity_);
    dtime_.SetSize    (g_.VrtxSize(), forever_);
    parent_.SetSize   (g_.VrtxSize(), null_);
    color_.SetSize    (g_.VrtxSize(), 'w');   // 'w' = white
  }
  else
  {
    for (Vertex x = 0; x < g_.VrtxSize(); ++x)
    {
      distance_[x]  = infinity_;  // unreachable distance
      dtime_[x]     = forever_;   // unreachable time
      parent_[x]    = null_;      // undefined parent
      color_[x]     = 'w';        // 'w' = white
    }
  }
}

template < class G >
void BFSurvey<G>::Reset( Vertex start )
{
  start_ = start;
  Reset();
}

Note that Reset allows the possibility that the graph has changed vertex size. But we don't change the reference to the graph itself. If we want to survey a different graph we use a different survey instance.

The Search(v) method is the same algorithm discussed above, enhanced to update all of the class data:

template < class G >
void BFSurvey<G>::Search( Vertex v )
{
  distance_[v] = 0;
  dtime_[v] = time_++;
  conQ_.PushBack(v);
  color_[v] = 'g';   // 'g' = grey
  Vertex front;
  AdjIterator i; 
  while (!conQ_.Empty())
  {
    front = conQ_.Front();
    // add all unvisited neighbors of front to queue
    for ( i = g_.Begin(front); i != g_.End(front); ++i )
    {
      if ('w' == color_[*i]) // 'w' = white = unvisited
      {
        distance_[*i] = distance_[front] + 1;
        dtime_[*i] = time_++;
        parent_[*i] = front;
        color_[*i] = 'g';    // 'g' = grey
        conQ_.PushBack(*i);
      }
    }
    // remove front of queue
    conQ_.PopFront();
    color_[front] = 'b';     // 'b' = black
  }
}

The Search() method calls Search(v) until all vertices have been visited:

template < class G >
void BFSurvey<G>::Search()
{
  Reset();
  for (Vertex v = start_; v < g_.VrtxSize(); ++v)
  {
    if (color_[v] == 'w') // 'w' = white = unvisited
      Search(v);
  }
  for (Vertex v = 0; v < start_; ++v)
  {
    if (color_[v] == 'w') // 'w' = white = unvisited
      Search(v);
  }
}

Search() is divided into two loops only to accomodate the starting vertex. These loops typically encounter few white vertices after the first call to Search(v). For example, if the graph is undirected and connected then every vertex is reachable from any start vertex, so only the first call to Search(v) is activated. Nevertheless, to be sure that all vertices are visited, we have to check each one for color.

Note that Reset() is called at the beginning of Search() to initialize the survey data and the algorithm control data so that Search() starts out with a blank slate. On the other hand, it would not work for Search(v) to call Reset(), that would blank out any survey data from previous calls to Search(v).

So: if a client wants to run Search(v) for one particular vertex, the client must first make the call to Reset explicitly.

Technical Upgrades. In practice, these technical upgrades to the code should be made:

  1. Make casts between N = Vertex and size_t explicit.
  2. Move the data arrays into private access control and provide accessor methods to prevent client tampering with data.

We left these out of the displayed code for readability. The library versions contain the upgrades, as well as an instrumentation option for examining specific runs of the algorithm.

2.4 Conclusions

Theorem 2s. The runtime of BFSurvey::Search(v) is O(|V| + |E|).

Theorem 2f. The runtime of BFSurvey::Search() is Θ(|V| + |E|).

(Here 's' is for "single search" and 'f' is for "full survey". Proofs of these results are interwoven into 2.1-2.3.)

2.5 Exercises

  1. Create the class BFSurvey in the file bfsurvey.h. The class should be in the fsu namespace.
    Instructions: Do not copy/paste from these notes and do not copy code from another source. TYPE all of the code using a simple text editor, and make sure that you understand what you are typing. Every token should come out of your brain through your fingers into a text file.

  2. Locate the source code file fbfsurvey.cpp and compile in the directory with graph.h and bfsurvey.h. Debug bfsurvey.h as necessary.

  3. Create two versions of fbfsurvey.cpp -- fbfsurvey_ug.cpp and fbfsurvey_dg.cpp -- by commenting/uncommenting the undirected and directed cases, respectively. Compile these to fbfs_ug.x and fbfs_dg.x respectively.

  4. Create a makefile to build these executables:

    fgraph.x    # dependencies: graph.h fgraph.cpp 
    agraph.x    # dependencies: LIB/notes_support/agraph_i.x
    rangraph.x  # dependencies: LIB/notes_support/rangraph_i.x
    fbfs_dg.x   # dependencies: graph.h bfsurvey.h fbfsurvey_dg.cpp
    fbfs_ug.x   # dependencies: graph.h bfsurvey.h fbfsurvey_ug.cpp
    

    The "builds" for agraph.x and rangraph.x just use a copy command followed by change of permissions. For example:

    rangraph.x: $(LIB)/notes_support/rangraph_i.x
            cp $(LIB)/notes_support/rangraph_i.x rangraph.x
            chmod 700 rangraph.x
    

    Use these executables, your previously created graph data files, and any new graph data files you deem appropriate, to test all aspects of your code. It is a good idea to have small graph files whose outcomes can be checked by hand, as well as some large files to test the robustness of your code.

3 Depth-First Search

Like BFS, the Depth-First Search [DFS] process begins at a vertex of G and explores the graph from that vertex. In contrast to BFS, which considers all adjacent vertices before proceeding deeper into the graph, DFS follows as deep as possible into the graph before backtacking to an unexplored possibility.

One way to understand the relationship between BFS and DFS begins with the following refactoring of the code for BFSearch(v):

BFSearch(v)
{
  conQ_.Push(v);          // font or back - conQ_ is empty
  color_[v] = grey;
  parent_[v] = null_;
  while (!conQ.Empty())
  {
    Vertex x = conQ.Front();
    if (n = unvisited adjacent from x)
    {
      conQ.PushBack(n);  // Push at back end
      color_[n] = grey;
      parent_[n] = x;
    }
    else
    {
      conQ.PopFront();   // Pop at opposite end  - Queue behavior
      color_[x] = black;
    }
  }
}

Taking this code for BFSearch(v) and making one change results in a DFSearch(v) algorithm:

DFSearch(v)
{
  conQ_.Push(v);          // font or back - conQ_ is empty
  color_[v] = grey;
  parent_[v] = null_;
  while (!conQ.Empty())
  {
    Vertex x = conQ.Front();
    if (n = unvisited adjacent from x)
    {
      conQ.PushFront(n);  // Push at front end
      color_[n] = grey;
      parent_[n] = x;
    }
    else
    {
      conQ.PopFront();    // Pop at same end  - Stack behavior
      color_[x] = black;
    }
  }
}

It is surprising how similar these two code blocks are. The algorithms behave in very different ways. The comparison does serve to emphasize the essential difference: BFSearch(v) is queue controlled whereas DFSearch(v) is stack controlled.

The optimized version of BFS discussed in Section 2 is much simpler in that we explicitly push all unvisited neighbors of the front of the queue immediately, which allows us to show that only one traversal of each neighbor list is required to implement "unvisited adjacent from front" and ultimately conclude that the runtime of BFSearch(v) is O(|V| + |E|). We will need to provide a way to find the first unvisited neighbor of the top of the stack in order to reach the same conclusion for DFSearch(v).

3.1 DFSearch(v)

The pseudo-code listed above for DFSearch(v) converts directly into actual code. The main obstacle to an efficient implementation is finding an implementation of "next unvisited neighbor" of the top of the control stack whose aggregate cost is O(|V|). We accomplished this for BFS by refactoring BFSearch(v) to traverse the neighbor list of the front of the queue one time only.

This solution does not carry over to DFS, because the top of the control stack is changed by a push operation, so we cannot predict which neighbor list is needed to find the next unvisited. Three possible solutions to this problem may be advanced:

  1. Maintain an array of AdjIterators pointing into all of the adjacency lists.
  2. Modify the algorithm so that "visited" occurs when a vertex is popped from the control stack, that is, equate "visited" with color_[v] = black.
  3. Convert to a recursive implementation.

Re 1: This is the approach we take. The iterators in the array advance in a somewhat ragged fashion, but they always advance, so that when the search process halts each of the adjacency lists has been traversed at most one time, at an aggregate cost bounded above by

vOutDegree(v) ≤ 2×|E|

(again using Theorem 1 for to get the 2×|E| bound). Just as in BFSearch, each vertex is processed through the stack at most one time (one Push and one Pop), so the total cost of DFSearch(v) is

2×|V| + 2×|E| ≤ O(|V| + |E|)

The use and maintenance of the array of adj iterators adds explicitly to the space overhead and intricacy of the algorithm.

Re 2: The problem with this implementation is that the size of the control stack is limited only by the number of edges. Really it's a kluge.

Re 3: This is the approach taken in [Cormen et al (2009), Introduction to Algorithms (3rd ed.), MIT Press, Cambridge, MA] and [Weiss (2014), Data Strucrures and Algorithms in C++ (4th ed.), Pearson Education/Addison-Wesley, Upper Saddles River, NJ]. The explicit space overhead in the iterative approach is hidden in the recursive call activation records, making the algorithm simpler to code and explain. (But also easier to "gloss over" details of how the algorithm actually works.)

There is some commentary, for example in the Wikipedia entry for DFS, that if the recursive implementation is the "real" DFSearch then the iterative implementation is not because it considers the neighbor vertices in a different (actually, reversed) order. This is a distinction without a difference, considering that (a) the order of vertices in an adjacency list is user-dependent and (b) in any case the order of consideration is easily reversed because AdjIterator is bidirectional. In fact, some have advocated making AdjIterators randomized, which would make distinctions of order in adjacency lists dissappear entirely.

By way of example, here is the result of running DFSearch(5) on the graph G1:

adj list rep       Graph G1                       DFS::conStack
------------       --------                       -------->
v[0]: 1 , 3                                       null           ...
v[1]: 0            0 --- 1     2 --- 6 --- 7      5              5
v[2]: 5 , 6        |           |           |      5 2            5 4
v[3]: 0 , 4        |           |           |      5 2 6          5 4 3
v[4]: 3 , 5        3 --- 4 --- 5 --- 8 --- 9      5 2 6 7        5 4 3 0
v[5]: 2 , 4 , 8                                   5 2 6 7 9      5 4 3 0 1
v[6]: 2 , 7                                       5 2 6 7 9 8    5 4 3 0
v[7]: 6 , 9                                       5 2 6 7 9      5 4 3
v[8]: 5 , 9                                       5 2 6 7        5 4
v[9]: 7 , 8                                       5 2 6          5
                                                  5 2            null
                                                  ...            
  Vertex discovery order: 5 2 6 7 9 8 4 3 0 1
  Vertex finishing order: 8 9 7 6 2 1 0 3 4 5

3.2 DFSurvey

The depth-first survey class definition is completely analogous to that defining breadth-first survey. The notion of distance is not as relevant to DFS and is omitted. But there are two "time" arrays, keeping track of discovery time of a vertex v (when v is pushed onto the control stack) and finishing time (when v is popped off the control stack). The private method NextUnvisitedNeighbor(x) returns the next unvisited neighbor of x, facilitated by the array of AdjIterators named nun_:

  template < class G >
  class DFSurvey
  {
  public:
    typedef G                           Graph;
    typedef typename Graph::Vertex      Vertex;
    typedef typename Graph::AdjIterator AdjIterator;

    DFSurvey    ( const Graph& g );
    DFSurvey    ( const Graph& g , Vertex start );
    void Search ( );
    void Search ( Vertex v );
    void Reset  ( );
    void Reset  ( Vertex start );

    fsu::Vector < Vertex >        dtime_;     // discovery time
    fsu::Vector < Vertex >        ftime_;     // finishing time
    fsu::Vector < Vertex >        parent_;    // for DFS tree
    fsu::Vector < char >          color_;     // various uses

  private:
    // data
    const Graph&                  g_;
    Vertex                        start_;     // default is vertex 0
    size_t                        time_;      // global sequencing clock
    size_t                        forever_;   // unreachable time
    Vertex                        null_;      // undefined vertex
    fsu::Vector < AdjIterator >   nun_;       // platoon of iterators supporting NextUnvisitedNeighbor
    fsu::Deque  < Vertex >        conQ_;      // control stack

    // method
    AdjIterator  NextUnvisitedNeighbor(Vertex x);      // returns iterator to next unvisited neighbor of x
  };

The notion of time is analagous to that of BFSurvey. However note that each vertex x gets two times assigned - discovery time dtime_[x], when x enters the control stack, and finishing time ftime_[x], when x departs the control stack. Thus the time instances used in the algorithm are those in the range [0..2×|V|). The first unreachable time is forever_ = 2*g_.VrtxSize().

3.3 Implementing DFSurvey

All of the method implementations are completely analogous to those of BFSurvey with three exceptions: (1) Search(v) follows the DFS algorithm described above; (2) constructors and Reset methods must add initialization of the vector nun_ to their task lists; and (3) there is a new method NextUnvisitedNeighbor(x) which returns an iterator pointing to the next unvisited neighbor of x. The portion of the code added by these exceptions is displayed in blue.

template < class G >
DFSurvey<G>::DFSurvey (const Graph& g)
  : g_(g), start_(0), time_(0), forever_(2*g_.VrtxSize()), null_((Vertex)g_.VrtxSize()),
    dtime_  (g_.VrtxSize(), forever_),
    ftime_  (g_.VrtxSize(), forever_),
    parent_ (g_.VrtxSize(), null_),
    color_  (g_.VrtxSize(), 'w'),   // 'w' = white
    nun_    (g_.VrtxSize()),
    conQ_()
{
  for (Vertex x = 0; x < g_.VrtxSize(); ++x)
    nun_[x] = g_.Begin(x);
}

template < class G >
DFSurvey<G>::DFSurvey (const Graph& g , Vertex start )
  : g_(g), start_(start), time_(0), forever_(2*g_.VrtxSize()), null_((Vertex)g_.VrtxSize()),
    dtime_  (g_.VrtxSize(), forever_),
    ftime_  (g_.VrtxSize(), forever_),
    parent_ (g_.VrtxSize(), null_),
    color_  (g_.VrtxSize(), 'w'),   // 'w' = white
    nun_    (g_.VrtxSize()),
    conQ_()
{
  for (Vertex x = 0; x < g_.VrtxSize(); ++x)
    nun_[x] = g_.Begin(x);
}

template < class G >
void DFSurvey<G>::Reset()
{
  time_ = 0;
  conQ_.Clear();
  if (color_.Size() != g_.VrtxSize()) // g_ has changed vertex size
  {
    forever_ = 2*g_.VrtxSize();        // last time stamp is 2|V| -1
    null_    = (Vertex)g_.VrtxSize();  // |V| is not valid vertex number
    dtime_.SetSize  (g_.VrtxSize(), forever_);
    ftime_.SetSize  (g_.VrtxSize(), forever_);
    parent_.SetSize (g_.VrtxSize(), null_);
    color_.SetSize  (g_.VrtxSize(), 'w');  // 'w' = white
    nun_.SetSize    (g_.VrtxSize());
    for (Vertex x = 0; x < g_.VrtxSize(); ++x)
      nun_[x] = g_.Begin(x);
  }
  else
  {
    for (Vertex x = 0; x < g_.VrtxSize(); ++x)
    {
      dtime_[x]  = forever_;
      ftime_[x]  = forever_;
      parent_[x] = null_;           
      color_[x]  = 'w';  // 'w' = white
      nun_[x]    = g_.Begin(x);
    }
  }
}

template < class G >
void DFSurvey<G>::Reset( Vertex start )
{
  start_ = start;
  Reset();
}

template < class G >
void DFSurvey<G>::Search( Vertex v )
{
  dtime_[v] = time_++;
  conQ_.PushBack(v);
  color_[v] = 'g';         // 'g' = grey
  Vertex top;
  AdjIterator i; 
  while (!conQ_.Empty())
  {
    top = conQ_.Back();
    i = NextUnvisitedNeighbor(top); // an iterator!
    if (i != g_.End(top))
    {
      dtime_[*i] = time_++;
      conQ_.PushBack(*i);
      parent_[*i] = top;
      color_[*i] = 'g';    // 'g' = grey
    }
    else
    {
      conQ_.PopBack();
      color_[top] = 'b';   // 'b' = black
      ftime_[top] = time_++;
    }
  }
}

template < class G >
void DFSurvey<G>::Search()
{
  Reset();
  for (Vertex v = start_; v < g_.VrtxSize(); ++v)
  {
    if (color_[v] == 'w') // white = unvisited
      Search(v);
  }
  for (Vertex v = 0; v < start_; ++v)
  {
    if (color_[v] == 'w') // white = unvisited
      Search(v);
  }
}

template < class G >
typename DFSurvey<G>::AdjIterator DFSurvey<G>::NextUnvisitedNeighbor (Vertex x)
{
  // Note: nun_[x] is already initialized and part way through a traversal!
  // This loop just advances to the next unvisited (white) neighbor of x
  // Total cost for entire survey is a single traversal of each adjacency list = Theta(|E|)
  while (nun_[x] != g_.End(x) && 'w' != color_[*nun_[x]])
    ++nun_[x];
  return nun_[x];
}

3.4 Conclusions

Theorem 3s. The runtime of DFSurvey::Search(v), as implemented with an array of AdjIterators, is O(|V| + |E|).

Theorem 3f. The runtime of DFSurvey::Search(), as implemented with an array of AdjIterators, is Θ(|V| + |E|).

3.5 Exercises

  1. Verify that the version of BFSearch(v) listed at the beginning of this Section 3 is functionally identical to the version described earlier as part of the implementation of BFSurvey (the listing in Section 2.3).

  2. Verify that the version of DFSearch(v) listed at the beginning of this Section 3 is functionally identical to the version described later as part of the implementation of DFSurvey (the blue listing in Section 3.3).

  3. Give an informal proof of Theorem 3.

  4. Explain why the runtime of DFSurvey::Search(v) would be Ω(|V| + |E|2) without the use of the helper array of adj iterators.

  5. Create the class DFSurvey in the file dfsurvey.h. The class should be in the fsu namespace.
    Instructions: Do not copy/paste from these notes and do not copy code from another source. TYPE all of the code using a simple text editor, and make sure that you understand what you are typing. Every token should come out of your brain through your fingers into a text file.

  6. Locate the source code file fdfsurvey.cpp and compile in the directory with graph.h and dfsurvey.h. Debug dfsurvey.h as necessary.

  7. Create two versions of fdfsurvey.cpp -- fdfsurvey_ug.cpp and fdfsurvey_dg.cpp -- by commenting/uncommenting the undirected and directed cases, respectively.

  8. Add two targets to your makefile for DFS, so that you now have these:

    fgraph.x    # dependencies: graph.h fgraph.cpp 
    agraph.x    # dependencies: LIB/notes_support/agraph_i.x
    rangraph.x  # dependencies: LIB/notes_support/rangraph_i.x
    fbfs_dg.x   # dependencies: graph.h bfsurvey.h fbfsurvey_dg.cpp
    fbfs_ug.x   # dependencies: graph.h bfsurvey.h fbfsurvey_ug.cpp
    fdfs_dg.x   # dependencies: graph.h dfsurvey.h fdfsurvey_dg.cpp
    fdfs_ug.x   # dependencies: graph.h dfsurvey.h fdfsurvey_ug.cpp
    

    Use these executables, your previously created graph data files, and any new graph data files you deem appropriate, to test all aspects of your DFS code. It is a good idea to have small graph files whose outcomes can be checked by hand, as well as some large files to test the robustness of your code.