Graphs/Depth-First Search

Lesson 6.23,131 words

Depth-First Search

Swap BFS's queue for a stack and the search plunges instead of fanning out. Depth-first search stamps every vertex with discovery and finish times that nest like parentheses, classifies each edge as tree, back, forward, or cross, and — through the back edge — decides in one pass whether a graph has a cycle.

╌╌╌╌

This builds on Graph Representations and Traversal, which set up graphs, the two representations, and the skeleton read with a queue as breadth-first search. Here we read the same skeleton with a stack, and the change of order exposes an entirely different structure.

Where BFS fans out in rings, depth-first search (DFS) plunges. It is Whatever-First-Search with a stack: pulling the newest discovered vertex means we always descend from where we just were. From a vertex it follows an edge to an unvisited neighbor, then a neighbor of that, going as deep as possible before backtracking to the most recent vertex with an unexplored edge. The LIFO stack is usually left implicit — it is the recursion call stack.

DFS produces a pair of timestamps on each vertex : a discovery (or start) time, which we write , stamped when first turns gray, and a finish time , stamped when turns black after all its descendants are done. A single global clock ticks once per stamp, so on vertices every value in is used exactly once. Unlike BFS, the outer driver restarts DFS from any leftover white vertex, so it covers disconnected pieces too, producing a depth-first forest rather than a single tree.

Algorithm 4:DFS(G)\textsc{DFS}(G) — discovery / finish times for every vertex
  1. 1
    foreach vertex uVu \in V do
  2. 2
    u.colorwhiteu.color \gets \text{white}
  3. 3
    u.πnilu.\pi \gets \text{nil}
  4. 4
    time0time \gets 0
  5. 5
    foreach vertex uVu \in V do
  6. 6
    if u.color=whiteu.color = \text{white} then
  7. 7
    call DFS-Visit(G,u)\textsc{DFS-Visit}(G, u)
Algorithm 5:DFS-Visit(G,u)\textsc{DFS-Visit}(G, u) — explore everything reachable from uu
  1. 1
    timetime+1time \gets time + 1
  2. 2
    u.stimeu.s \gets time
    discover u (turns gray)
  3. 3
    u.colorgrayu.color \gets \text{gray}
  4. 4
    foreach vv adjacent to uu do
  5. 5
    if v.color=whitev.color = \text{white} then
  6. 6
    v.πuv.\pi \gets u
  7. 7
    call DFS-Visit(G,v)\textsc{DFS-Visit}(G, v)
  8. 8
    u.colorblacku.color \gets \text{black}
  9. 9
    timetime+1time \gets time + 1
  10. 10
    u.ftimeu.f \gets time

Like BFS, DFS runs in : the initialization and the outer loop cost , and is called exactly once per vertex (only on white vertices, which it immediately grays), scanning each adjacency list once for total.

A worked DFS, and the parenthesis structure

Run on the digraph below. The outer loop scans vertices in the order , and each adjacency list is scanned alphabetically. The clock ticks once per stamp:

  • : discover . Its list is ; the first neighbor is white.
  • : discover (so ). Its list is ; is white.
  • : discover . Its only neighbor is white.
  • : discover . No white neighbors.
  • : finish . : finish — back in , whose next neighbor is still white.
  • : discover . Its list is : is gray (an active ancestor), is black — neither is entered, but both edges get classified below.
  • : finish . : finish — back in , whose remaining neighbor is now black.
  • : finish . The outer loop resumes and finds still white.
  • : discover ; /: discover and finish ; /: discover and finish (its neighbor is black); : finish .

Each vertex below is labeled with its interval as discovery/finish. Thick edges are tree edges (the depth-first forest — two trees here, rooted at and ); dashed edges are the non-tree edges, tagged B (back), F (forward), or C (cross) and classified in the next section.

DFS forest on a digraph with discovery/finish times and non-tree edges tagged B (back), F (forward), C (cross).

The discovery and finish times nest like balanced parentheses. Write each vertex's interval . Reading the trace above as brackets — open at discovery, close at finish — gives

a well-formed string: intervals can never partially overlap. This is the parenthesis theorem (Erickson calls it the nesting lemma).1

It makes the recursion visible. Drawing each interval as a bar along the time axis turns the forest into a literal stack of nested brackets — a bar sits strictly inside another exactly when the inner vertex is a descendant of the outer:

DFS intervals drawn as bars on the time axis; one bar nesting inside another marks a descendant.

Above, , so is a descendant of is a descendant of . Two companions sharpen it.

This is the cleanest test for what will end up under .

This makes look for a back edge a decision test for cyclicity, and the two clauses are precisely its soundness and completeness. Every back edge lies on a cycle is soundness: when DFS flags a back edge, a genuine cycle is there — no false alarm. Every cycle contains a back edge is completeness: no cycle slips past the search. Together they underlie cycle detection and topological sort.

Classifying edges

As DFS traverses an edge , the color of at that moment reveals what kind of edge it is, a classification that drives the algorithms of the next lessons. In the worked digraph above:

  • Tree edge. is white: we set , so the edge is . We discover through this edge; it joins the depth-first forest (e.g. , ).
  • Back edge. is gray, an ancestor of still on the recursion stack (, since ). A digraph has a back edge iff it has a cycle, the linchpin of cycle detection and topological sort.
  • Forward edge. is black and a descendant of (: a non-tree edge to an already-finished descendant).
  • Cross edge. is black and not a descendant of (, or the edge between separate tree branches).

The color test runs during the traversal; once DFS finishes, the timestamps alone decide, courtesy of the parenthesis theorem. For an edge :

ClassInterval relationColor of when scanned
tree / forwardwhite / black
backgray
crossblack

(Tree and forward edges share the interval pattern — a descendant of — and are told apart by .) The fourth conceivable pattern, with finishing after , never occurs for an edge: if is discovered while is active, the white-path property pulls into 's subtree, so finishes first. A minimal example exhibiting all four classes:

All four edge classes on one small digraph. DFS starts at , then restarts at ; vertices show discovery/finish times.

DFS from walks the chain ; the edge finds gray (back), and on the way out finds black inside 's interval (forward). The restart at finds black with (cross).

In an undirected graph the picture simplifies: every edge is either a tree edge or a back edge, since forward and cross edges cannot occur, because exploring an edge from either endpoint reaches the other while it is still gray.

Worked example: connected components

The outer driver — restarting DFS from any leftover white vertex — labels connected components for free. Each restart begins a new tree, and every vertex it reaches shares the same component. Take an undirected graph on seven vertices with edges , , (a triangle), the lone edge , and the isolated pair . Scanning vertices alphabetically:

  • Restart 1 at (component ). DFS visits , then , then ; from the edge finds gray, a back edge that closes the triangle but visits nothing new. Component .
  • Restart 2 at (component ). DFS visits , then across the single edge; has no other neighbor. Component .
  • Restart 3 at (component ). DFS visits , then . Component .

Three restarts, three components. The number of times the outer loop calls from a white vertex is the number of connected components, computed in the same pass — no extra structure needed beyond a component counter incremented at each restart.

DFS labels connected components for free. Each restart of the outer loop from a white vertex (arrowed) begins a new tree; here three restarts at , , discover the three components , , .

The dashed edge is the triangle's one back edge; every other edge is a tree edge, matching the undirected rule above. On a directed graph the same restart-counting overcounts — reachability is one-way — the gap that strongly connected components close with a second DFS pass.

Common pitfalls

Traversal code is short, and most of its bugs hide in the two lines everyone writes from memory.

  • Marking visited at dequeue instead of enqueue. If BFS checks the color only when a vertex leaves the queue, a vertex can be enqueued once per incoming edge before its first copy is processed. Distances still come out right (the first copy dequeued carries the smallest ), but the queue swells to entries. Discover-time marking — gray the vertex the moment it is first seen — is what keeps each vertex enqueued once true.
  • No visited check at all. On any graph with a cycle the traversal loops forever; on a dense DAG it can revisit vertices exponentially often. The visited flag is the entire difference between graph search and blind walking.
  • Undirected graphs stored one-way. An undirected edge must appear on both adjacency lists. Insert only and the graph silently becomes a digraph; BFS distances and connectivity both come out wrong, and nothing crashes to tell you.
  • Recursive DFS on deep graphs. A path of vertices means recursion depth , which overflows the call stack in most language runtimes long before memory runs out. Production DFS either raises the recursion limit or maintains an explicit stack of (vertex, position-in-adjacency-list) frames — and the naive push all neighbors stack version visits vertices in a valid DFS order but does not reproduce the recursive discovery/finish times, so timestamp-based arguments quietly break.
  • Parent edges masquerading as cycles. In an undirected graph, DFS from immediately re-sees the edge back to its parent , which is gray. That is not a cycle; cycle detection must ignore the single edge to the parent (and be careful again if parallel edges are allowed).
  • Trusting vertex order. Adjacency-list order changes the BFS tie-breaking order within a layer and the shape of the DFS forest. Distances and the parenthesis structure are guaranteed; which shortest-path tree or which edge classification you get is not, so tests that hard-code one particular tree are brittle.

BFS or DFS?

They are one algorithm, , read with two different bags. Both are linear-time skeletons; the order of removal is the only difference, and it dictates the structure each exposes.

BFS (queue)DFS (stack)
Frontier bagqueue, FIFO — oldest firststack, LIFO — newest first
Search treeshallowest paths from nesting / parenthesis structure
Computes (shortest hops)start/finish times
Revealslevel sets, connectivityback edges → cycles, finish order
Typical usesunweighted shortest paths, componentscycle detection, topological sort, strong connectivity

Choose BFS when distance-in-hops matters; choose DFS when you need a graph's recursive structure, which, as the next two lessons show, is what topological sorting and strong connectivity demand. And keep the frame in mind: swap the bag for a priority queue and the very same skeleton becomes Dijkstra and Prim.

What DFS unlocks

Almost every algorithm in the rest of this module is DFS with a small amount of bookkeeping added. The timestamps and the edge classification are the raw material.

Cycle detection, traced. The cycle theorem (acyclic iff no back edge) is a decision procedure. Take the dependency graph with a side branch . DFS from discovers (gray), then (gray), then (gray); 's only edge is , and is gray — an ancestor still on the recursion stack — so is a back edge and a cycle is reported the instant it is scanned. The gray set at that moment, , holds exactly the cycle's vertices. Contrast a graph with instead of : every scanned edge lands on a white or black vertex, DFS finds no back edge, and the finish order (reverse of finishing) is a valid topological order.

Cycle detection by back edge. Left: finds gray (on the stack), so it is a back edge and is a cycle. Right: replacing it with leaves no gray target, no back edge, and DFS finishing order gives a topological sort.

Iterative DFS. The recursive form is the clearest, but a path of a million vertices overflows the call stack in most runtimes. The fix is an explicit stack of frames — a (vertex, iterator-into-its-adjacency-list) pair — so the recursion lives on the heap. The subtlety worth flagging: the naive push all neighbors at once stack does visit vertices in a valid DFS order, but it does not reproduce the discovery/finish timestamps, because a vertex can be pushed several times before it is first popped. Any argument that leans on the parenthesis structure (edge classification, low-link values) needs the frame-based iterative form that stamps finish times on the way back down the stack.

The rest of the module, in one sentence each. Every algorithm ahead is DFS plus a specific piece of bookkeeping:

  • Topological sort and SCC orders a DAG by reverse finish time, and finds strongly connected components by running DFS twice (Kosaraju) or tracking low-links in one pass (Tarjan).
  • Bridges and articulation points add a low[v] value — the earliest discovery time reachable from 's subtree via one back edge — to find the edges and vertices whose removal disconnects the graph.
  • Lowest common ancestor and Eulerian tours both walk the DFS tree, one reading its ancestor structure, the other its edge traversal order.

DFS is a framework: run the depth-first walk, and decide what to record. The choice of bookkeeping is the design space.

Takeaways

  • A graph models things and their connections; the sparse-versus-dense distinction drives every representation choice.
  • Prefer the adjacency list ( space, fast neighbor iteration); use the adjacency matrix () only for dense graphs or constant-time edge tests.
  • is the one skeleton: grow a frontier bag, and let the bag's discipline pick the next vertex. A queue gives BFS, a stack gives DFS, a priority queue gives the weighted algorithms to come.
  • BFS explores in rings from a source and computes , the shortest path in hops, building a breadth-first tree via a FIFO queue.
  • DFS plunges and backtracks, stamping start/finish times that nest like parentheses (parenthesis + white-path theorems) and classify edges; a back edge exists exactly when the graph has a cycle.
  • Both traversals run in , linear in the graph's size, and either one labels connected components for free.

Footnotes

  1. Erickson, Ch. 6 — Graph Search — DFS start/finish times and the parenthesis (nesting) theorem.
Practice

╌╌ END ╌╌