Evanalysis
5.1Estimated reading time: 17 min

5.1 Graph representations, DFS, and BFS

Represent directed and undirected graphs precisely, trace depth-first and breadth-first search, and justify reachability, unweighted shortest paths, and O(V+E) traversal.

Course contents

A graph separates two questions that are easy to mix up: what relations exist, and how those relations are stored. Depth-first search (DFS) and breadth-first search (BFS) operate on the same abstract graph, but their frontiers impose different exploration orders. A rigorous trace must therefore state the graph representation, the order in which neighbours are inspected, and exactly when a vertex becomes discovered.

Motivation

Graphs model roads, communication links, prerequisites, and program states. Unlike an array, a graph does not come with one natural “next” element. From a vertex there may be no outgoing edge, one outgoing edge, or many. A traversal needs a disciplined frontier and a record of what has already been discovered.

The choice of frontier answers different questions:

  • DFS follows one unfinished route deeply and is naturally expressed by recursion or a stack.
  • BFS processes the oldest discovered vertex first and therefore expands in distance layers through a queue.

Neither algorithm requires edge weights merely to visit vertices. When every edge has equal cost, however, BFS provides more: it computes the minimum number of edges from the source to every reachable vertex.

Definitions

Definition

Graph and edge types

A graph is written G = (V,E), where V is a set of vertices and E is a set of edges. In a directed graph an edge (u,v) goes from u to v; it does not automatically permit travel from v to u. In an undirected graph an edge joins its endpoints in both directions. A weighted graph attaches a numerical weight to each edge; an unweighted graph does not distinguish edges by cost.

A path, in the course terminology, is a vertex sequence in which every consecutive pair is joined by an edge; it may repeat vertices. A simple path does not repeat vertices. A path's unweighted length is its number of edges, not its number of vertices. A cycle begins and ends at the same vertex; a simple cycle repeats no other vertex. A graph with no cycle is acyclic.

Vertex v is reachable from u if a directed path from u to v exists. Reachability need not be symmetric in a directed graph. In an undirected graph, two vertices are connected when a path joins them, and the graph is connected when every pair is connected. A maximal connected part is a connected component. For a directed vertex, out-degree counts leaving edges and in-degree counts entering edges.

Choosing a representation

Let the vertices be indexed 0,1,...,|V|-1.

Definition

Adjacency matrix

An adjacency matrix is a |V| × |V| array M in which M[u][v] records whether the edge (u,v) exists. In a weighted graph the entry can store the weight, together with an unambiguous convention for “no edge.” An undirected graph produces a symmetric matrix.

Definition

Adjacency list

An adjacency list stores, for every vertex u, a list Adj[u] of the vertices reached by outgoing edges from u. A weighted list stores a neighbour-weight pair for each outgoing edge.

The representation changes the cost of obtaining neighbours:

OperationAdjacency matrixAdjacency list
Store the graph`Theta(V
Test whether (u,v) existsTheta(1)O(out-degree(u))
Enumerate all outgoing neighbours of u`Theta(V
Traverse the whole graph`Theta(V

An adjacency matrix can be appropriate for a dense graph or repeated direct edge queries. An adjacency list avoids scanning absent edges and is therefore the natural representation for the linear-time traversals analysed below. In an undirected adjacency list, each edge appears in two lists; this changes a factor of two, not the asymptotic bound.

DFS discovers a vertex, chooses its first undiscovered neighbour, and suspends the current vertex while it explores that neighbour. Recursive calls remember the suspended work. When a vertex has no undiscovered neighbour left, the call finishes and control backtracks.

DFS-VISIT(u):
    discovered[u] = true
    process u
    for each v in Adj[u], in the stated order:
        if not discovered[v]:
            parent[v] = u
            DFS-VISIT(v)

DFS-FOREST(G):
    set every vertex to undiscovered and every parent to null
    for each vertex u in the stated vertex order:
        if not discovered[u]:
            DFS-VISIT(u)

Calling DFS-VISIT(s) explores exactly the vertices reachable from s. DFS-FOREST adds the outer loop needed to cover a disconnected graph. Each root produced by that loop begins another DFS tree. An explicit stack can replace recursion, but it must preserve the same neighbour-order convention if the same trace is desired: pushing several neighbours onto a last-in-first-out stack can reverse their processing order.

The parent edges form a DFS tree (or a forest under the outer loop). They record how each vertex was first discovered; non-tree edges remain part of the graph even though they are not parent edges.

Breadth-first search and its frontier invariant

BFS uses a first-in-first-out queue. The queue contains discovered vertices whose outgoing adjacency lists have not yet been completely processed. The safe discovery rule is crucial:

Mark a vertex discovered when it is enqueued, not later when it is dequeued.

BFS(G, s):
    for each vertex v:
        discovered[v] = false
        distance[v] = infinity
        parent[v] = null
    discovered[s] = true
    distance[s] = 0
    enqueue(Q, s)

    while Q is not empty:
        u = dequeue(Q)
        process u
        for each v in Adj[u], in the stated order:
            if not discovered[v]:
                discovered[v] = true
                distance[v] = distance[u] + 1
                parent[v] = u
                enqueue(Q, v)

The assignment to discovered[v] occurs before enqueue(Q,v). Thus a second frontier vertex that also has an edge to v sees that v is already claimed and cannot enqueue it again. “Discovered” and “fully processed” are different states: a queued vertex is discovered even though it has not yet been dequeued.

As with DFS, one call explores only vertices reachable from the source. To traverse every component of an undirected graph, run BFS again from an undiscovered vertex until none remain.

Theorem/proposition

Theorem

BFS shortest-path property for unweighted graphs

When BFS starts at s, every reachable vertex v is first discovered with distance[v] equal to the minimum number of edges on any path from s to v. Following parent pointers from v back to s reconstructs such a shortest path.

This statement is about equal-cost edges. If edges have unequal weights, a path with fewer edges can have greater total weight. Ordinary BFS then still minimizes edge count, but it does not necessarily minimize weighted cost.

Theorem

Adjacency-list traversal bound

DFS and BFS run in O(|V|+|E|) time on an adjacency-list graph when the search covers the whole graph. Their visited, parent, and frontier storage is O(|V|) in addition to the graph representation.

For a search from one source, the sharper bound uses only the reached vertices and scanned outgoing edges. The conventional whole-graph bound is stated because the outer-loop versions visit every component.

Proof sketch or proof idea

For the BFS shortest-path property, use a layer invariant. Initially only s has distance 0. Whenever BFS discovers v while processing u, it assigns distance[v] = distance[u] + 1. Because the queue is first-in-first-out, every vertex at distance k is processed before any newly discovered vertex at distance k+1. Therefore a hypothetical shorter path to v would have had to reach v from an earlier layer and would have discovered it sooner. The first assigned distance is consequently minimal. Each parent pointer decreases the distance by exactly one, so the reconstructed chain has that minimal length.

For the running time, initialization and the outer loop cost O(|V|). A vertex becomes discovered once, so it enters the DFS recursion/stack or BFS queue once. Scanning all adjacency lists examines each directed edge once; an undirected edge is examined twice. Hence the total is O(|V|+|E|). With a matrix, processing each reached vertex scans a full row of |V| entries, so a whole-graph traversal is instead Theta(|V|^2).

Worked examples

Use the course graph on vertices A,B,C,D,E,F. It is directed, and we fix the following adjacency-list order:

A: B, D
B: C, D
C: F, D, E
D: E
E: —
F: —

Worked example

Convert the course graph between representations

With row and column order A,B,C,D,E,F, the same graph has adjacency matrix

    A B C D E F
A   0 1 0 1 0 0
B   0 0 1 1 0 0
C   0 0 0 1 1 1
D   0 0 0 0 1 0
E   0 0 0 0 0 0
F   0 0 0 0 0 0

The matrix is not symmetric: for example A -> B exists but B -> A does not. The list makes the eight outgoing edges explicit without storing the twenty-eight absent ordered pairs.

Worked example

Trace recursive DFS from A

Using the fixed list order, the discoveries are A, B, C, F, D, E. The active recursive stack changes as follows:

EventActive stack after the event
discover A[A]
discover B[A,B]
discover C[A,B,C]
discover F[A,B,C,F]
finish F[A,B,C]
discover D[A,B,C,D]
discover E[A,B,C,D,E]
finish and backtrackeventually []

When control returns to C, its neighbour E is already discovered through D, so no second recursive call is made. A different adjacency-list order can produce a different valid DFS order without changing which vertices are reachable.

Worked example

Trace BFS layers, parents, and distances from A

The queue below is shown after the current vertex has been processed.

DequeuedNewly discoveredQueue afterwardNew distance/parent
AB,D[B,D]d(B)=d(D)=1, parents A
BC[D,C]d(C)=2, parent B
DE[C,E]d(E)=2, parent D
CF[E,F]d(F)=3, parent C
Enone[F]no change
Fnone[]no change

Thus the dequeue order is A,B,D,C,E,F. Parent pointers reconstruct, for example, the shortest path A -> B -> C -> F, containing three edges. Although C also has an edge to E, the route A -> B -> C -> E has three edges. BFS has already discovered E through the shorter two-edge route A -> D -> E.

Worked example

Why marking only on dequeue creates duplicates

Suppose A is marked only when removed from the queue. Processing A enqueues B and D, but leaves both unmarked. The queue is [B,D]. After B is removed, B examines C and D. Since the earlier copy of D is still unmarked, B enqueues D again, producing [D,C,D].

The graph has not changed; the implementation has lost the one-enqueue invariant. It may waste work and can overwrite a parent or distance unless more checks are added. Marking D at the moment A enqueues it makes B skip the duplicate. This is why enqueue-time marking is part of BFS correctness, not a minor coding preference.

Common mistakes

Common mistake

Treating a traversal order as unique

DFS and BFS respect their stack/queue disciplines, but ties are resolved by the stated adjacency order. Change that order and the exact trace may change.

Common mistake

Confusing discovery with completion

In BFS, a vertex becomes discovered when enqueued and becomes fully processed after it is dequeued and its adjacency list is scanned. Delaying discovery until dequeue permits duplicate queue entries.

  • A source search does not automatically visit unreachable vertices; an outer loop is required for a full graph traversal.
  • A directed matrix need not be symmetric, and directed reachability need not work in both directions.
  • BFS guarantees minimum edge count, not minimum arbitrary weighted cost.
  • Quoting O(|V|+|E|) while actually scanning every row of an adjacency matrix mixes two different representations.
  • A DFS parent tree or BFS parent tree contains discovery edges, not every edge of the original graph.

Summary

  • A graph may be directed or undirected and weighted or unweighted.
  • Paths, cycles, reachability, and connectivity describe the abstract graph, independently of its storage representation.
  • A matrix supports constant-time edge tests but costs quadratic space and row scans; a list stores and scans existing neighbours.
  • DFS uses recursion or a stack to go deep before backtracking.
  • BFS uses a queue to expand reachable vertices in nondecreasing edge-distance layers.
  • Robust BFS marks a vertex at enqueue time, guaranteeing at most one queue entry per vertex.
  • BFS parent and distance arrays describe shortest paths in an unweighted graph.
  • Adjacency-list DFS and BFS take O(|V|+|E|) for the whole graph.

Exercises

  1. For the AF graph, write the outgoing neighbours of C and the row of the adjacency matrix belonging to C. Explain why the list cost depends on out-degree while the matrix row scan does not.
  2. Run recursive DFS from D. Then run BFS from D. Give the discovery order for each search using the fixed list order, and state which vertices are not reachable.
  3. In the original BFS from A, explain why E receives distance 2 even though C also has an edge to E. Reconstruct the stored parent path.
  4. Give the earliest queue state that contains a duplicate when visited marks are delayed until dequeue. Identify the two edges responsible.
  5. A graph has 10,000 vertices and 20,000 directed edges. Compare the number of adjacency positions conceptually scanned by a whole-graph matrix traversal with the list-based asymptotic work.

Solutions

Solution · Guided solutions
  1. Adj[C] = [F,D,E], while row C is 0 0 0 1 1 1 in column order A,B,C,D,E,F. The list scans three stored neighbours; the matrix must inspect all six columns, including absent edges.

  2. From D, both DFS and BFS discover D,E, in that order. E has no outgoing edge. Vertices A,B,C,F are unreachable from D; incoming edges to D do not permit travel backwards in this directed graph.

  3. D is dequeued before C, so it discovers E first and assigns distance[E]=distance[D]+1=2 with parent[E]=D. When C is processed, E is already discovered. The stored path is A -> D -> E.

  4. After processing A under delayed marking the queue is [B,D]. Processing B follows B -> D while the copy inserted through A -> D is still unmarked, producing [D,C,D].

  5. A matrix traversal scans about |V|^2 = 100,000,000 positions. An adjacency list traversal performs O(|V|+|E|) = O(30,000) vertex-and-edge work, up to constant factors. The contrast comes from scanning only stored edges.

Practice

Work out your answer, then check it. You can revise and try again.

Loading…

Key terms in this unit