Back to Graph Theory Series

Part 6: Graph Traversal II — Depth-First Search

August 30, 2026 Wasil Zafar 20 min read

DFS's discovery/finish-time stamps are one of the most quietly powerful ideas in all of graph theory — a single number pair that unlocks cycle detection, topological order, and structural analysis all at once.

Table of Contents

  1. A Bit of History
  2. DFS in One Paragraph
  3. The Parenthesis Theorem
  4. Core Applications
  5. Toward Topological Order and SCCs
  6. Exercises
  7. Conclusion & Next Steps

A Bit of History

While DFS's traversal strategy dates back to Trémaux in the 1880s (see the DFS deep dive), the modern algorithmic toolkit built on top of it — discovery/finish times, edge classification, and the family of linear-time structural algorithms derived from them — was largely the work of Robert Tarjan, in a series of papers starting with his landmark 1972 result "Depth-First Search and Linear Graph Algorithms." In that single paper, Tarjan showed that DFS could find strongly connected components, biconnected components, and more, all in \(O(V+E)\) time — a result so influential it contributed to him sharing the 1986 Turing Award (with John Hopcroft) "for fundamental achievements in the design and analysis of algorithms and data structures." Nearly every algorithm in the remainder of this part, and in Part 8, traces its lineage to that one paper.

DFS in One Paragraph

Recall from the DFS deep dive: DFS explores as far as possible along one branch before backtracking, using a stack (explicit or via recursion). This part assumes that mechanic is already familiar and instead develops the bookkeeping — discovery time \(d[v]\), finish time \(f[v]\), and the four-way edge classification (tree/back/forward/cross) — that turns "a traversal order" into "a structural analysis tool."

Visual Intuition

DFS Dives Down One Branch, Then Rewinds

Think of DFS as carrying a stack of unfinished choices. It keeps pushing deeper vertices until the current vertex has nowhere new to go. Then it pops back to the most recent unfinished vertex and tries the next branch.

Current DFS branch The purple path is the active recursion/stack path. dead end → backtrack S A C E B D F G DFS first dives: S → A → C → E What the stack remembers Most recently entered vertex is on top. TOP E dead end C A S BOTTOM 1. E finishes and pops. 2. C becomes active again. 3. Try C's next unvisited neighbor.

The unifying mental model

A vertex is active from the moment DFS discovers it until the moment DFS finishes it. The active vertices form the current stack path. Discovery/finish times, the Parenthesis Theorem, and back-edge cycle detection all come from tracking this one stack.

The Parenthesis Theorem

Think of \(d[v]\) as an opening parenthesis and \(f[v]\) as its matching closing parenthesis. The Parenthesis Theorem states that for any two vertices \(u, v\), their intervals \([d[u], f[u]]\) and \([d[v], f[v]]\) are either completely disjoint or completely nested — one can never partially overlap the other. This single fact is why "is \(v\) a descendant of \(u\) in the DFS tree" reduces to a simple interval check: \(v\) is a descendant of \(u\) exactly when \(d[u] < d[v] < f[v] < f[u]\).

Discovery / Finish Times

The Parenthesis Theorem as a Timeline

Every DFS call opens when the vertex is discovered and closes only after all of its descendants have finished. That means child intervals must fit completely inside ancestor intervals.

DFS clock → 1234 5678 A A: [d=1, f=8] B B: [2,5] D D: [3,4] C C: [6,7] Nested: D inside B inside A Disjoint: B=[2,5] and C=[6,7] never partially overlap.

Ancestor test: if \(d[u] < d[v] < f[v] < f[u]\), then the entire lifetime of \(v\) occurs while \(u\) is still active, so \(v\) is a descendant of \(u\).

Reading the diagram above as intervals on a number line: \(A=(1,8)\), \(B=(2,5)\), \(D=(3,4)\), \(C=(6,7)\). Notice \(D\)'s interval sits entirely inside \(B\)'s, which sits entirely inside \(A\)'s — full nesting, confirming \(D\) is a descendant of both \(B\) and \(A\). Meanwhile \(B=(2,5)\) and \(C=(6,7)\) are completely disjoint — neither is an ancestor of the other, exactly as the tree structure shows.

Core Applications

DFS Spanning Forests

The tree edges discovered during a full DFS traversal (potentially restarting from a new unvisited vertex whenever the current tree is exhausted) form a DFS spanning forest — one tree per connected component, exactly analogous to a BFS spanning forest from Part 5, but shaped very differently (deep and narrow rather than broad and shallow).

Visual Intuition

One DFS Tree per Connected Component

DFS exhausts one connected component before it can move anywhere else. If unvisited vertices remain afterward, start a new DFS root. The collection of DFS trees is the spanning forest.

Component 1 → DFS tree rooted at A Component 2 → restart at F ABCDE FGHIJ No edge crosses between components, so DFS must restart.

Cycle Detection

In an undirected graph, a back edge to any vertex other than the immediate parent signals a cycle (a back edge to the parent itself is just the same edge traversed backward, not a real cycle). In a directed graph, any back edge — an edge to a vertex still "in progress" (on the current recursion stack) — signals a cycle; this is exactly the white/gray/black three-state coloring previewed in the DFS deep dive's exercises.

Cycle Intuition

A Back Edge Means “I Found My Way Back to an Active Ancestor”

The current DFS stack is a path from an ancestor down to the current vertex. If an edge points back to a vertex already on that path, the stack path plus that edge closes a loop.

Directed graph: gray = currently active on the DFS stack back edge D → B A B C D X Y Gray: active Black: finished White: undiscovered

Here B is still gray when DFS reaches D, so the edge \(D \to B\) closes the directed cycle \(B \to C \to D \to B\). That is why a directed graph is a DAG exactly when DFS finds no back edge.

Why This Matters: DAG Detection in One Pass

A directed graph is a DAG (Part 7) if and only if a single DFS traversal finds zero back edges — no separate algorithm needed. This is the cheapest possible way to validate that a dependency graph (build system, package manager, task scheduler) has no circular dependency before attempting to schedule anything.

Flood Fill

Case Study

The "Paint Bucket" Tool Is DFS on a Pixel Grid

Every image editor's paint-bucket / fill tool treats the image as an implicit grid graph — each pixel is a vertex, adjacent same-colored pixels are edges — and runs DFS (or BFS; either works, though DFS's smaller memory footprint on a recursive stack is common in simple implementations) from the clicked pixel, recoloring every reachable same-colored pixel. The classic 1990s-era recursive flood-fill routines that could crash on very large fill regions were hitting exactly the recursion-depth limitation discussed in the DFS deep dive — production image editors switched to iterative, explicit-stack DFS (or scanline-based variants) specifically to avoid that failure mode.

Before: connected same-color region After DFS from the clicked pixel click DFS visits every reachable same-color neighbor The image is an implicit graph: pixels are vertices; same-color neighboring pixels are edges.
Flood FillImage Editing

Toward Topological Order and Strongly Connected Components

Two results, stated here and proven in full in the next two parts, both flow directly from finish times: reverse finish-time order gives a valid topological sort of a DAG (Part 7) — every edge \((u,v)\) satisfies \(f[u] > f[v]\), so listing vertices from highest to lowest finish time respects every dependency. And Kosaraju's algorithm for finding strongly connected components (an upcoming deep dive) runs DFS twice — once on \(G\) to compute finish times, once on the transpose graph \(G^T\) in decreasing finish-time order — turning the same bookkeeping into a completely different structural result.

Why Finish Times Matter

Finishing Later Means “I Had to Wait for Everything Below Me”

In a DAG, a vertex cannot finish until every descendant reached through its outgoing edges has finished. That naturally pushes prerequisites with larger finish times toward the front when we reverse the finish order.

Topological order from finish times Example DAG (Directed Acyclic Graph) A B C D E f=10 f=8 f=6 f=4 f=2 Descending finish time: A, B, C, D, E Kosaraju: same clock, different job Finish times choose the order for the second DFS. 1. DFS on G Record f[v] 2. Transpose Reverse edges 3. DFS on Gᵀ Largest f first Why the order matters • First Pass: Arranges SCCs by their exit structure. • Transpose: Prevents cross-component traversal. • Second Pass: Runs in descending finish order to visit exactly one strongly connected component at a time.

Exercises

  1. Run DFS by hand on a small directed graph with a cycle, and identify exactly which edge is the back edge that reveals it.
  2. Using the Parenthesis Theorem, determine (without drawing the tree) whether a vertex with interval \((4,9)\) is an ancestor, descendant, or unrelated to a vertex with interval \((2,10)\).
  3. Explain why an undirected graph's DFS forest never contains forward or cross edges — only tree edges and back edges (hint: think about what "cross edge" would even mean when every edge is bidirectional).
  4. Challenge: Implement flood fill both recursively and iteratively on a large synthetic grid (e.g., 1000×1000, all one color) and measure at what grid size the recursive version fails in your language of choice.

Conclusion & Next Steps

Discovery/finish times and the Parenthesis Theorem turn DFS from "a traversal" into a structural probe: cycle detection, DAG validation, and (as previewed) topological sorting and strongly-connected-component analysis are all one small extension away. We formalize the first of those — DAGs and topological order — next.

Next in the Series

In Part 7: DAGs, Topological Sorting & Critical Path Method, we formalize dependency graphs, prove when a topological order exists, and use it to schedule real projects with the Critical Path Method.