DSA DSA In Depth
0%
lesson dfsedge-classificationgraph-theory

Depth-First Search: Parenthesis Theorem, White-Path Theorem & Tree/Back/Forward/Cross Edges

7 min read Module 5.1: Graph Representations, Traversals, Connectivity & Topological Order Updated Oct 23, 2026
📄 Cheat Sheet
Depth First Search (DFS) Algorithm | Graph Theory
WilliamFiset Depth First Search (DFS) Algorithm | Graph Theory Watch on YouTube ↗

Covers DFS discovery/finish timestamps, the Parenthesis Theorem, and edge classification proofs.

0:00 / --:--

Temporal Coordinate System & The Three-Color Invariant

During a DFS traversal of a directed or undirected graph $G = (V, E)$, every vertex $u \in V$ transitions monotonically through three states:

  • WHITE ($0$): Undiscovered ($t < \text{tin}[u]$).
  • GRAY ($1$): Discovered and currently active on the recursion stack ($\text{tin}[u] \le t < \text{tout}[u]$). The set of all GRAY vertices at any instant forms the exact path from the current DFS root to the currently executing vertex.
  • BLACK ($2$): Fully explored and popped from the recursion stack ($t \ge \text{tout}[u]$).
Formal Theorems — Parenthesis Theorem & White-Path Theorem

Parenthesis Theorem (CLRS Theorem 20.7): In any DFS of a (directed or undirected) graph $G$, for any two vertices $u$ and $v$, exactly one of three conditions holds for their intervals $I_u = [\text{tin}[u], \text{tout}[u]]$ and $I_v = [\text{tin}[v], \text{tout}[v]]$:

  1. $I_u$ and $I_v$ are completely disjoint, and neither $u$ nor $v$ is a descendant of the other in the DFS forest.
  2. $I_v \subset I_u$ ($\text{tin}[u] < \text{tin}[v] < \text{tout}[v] < \text{tout}[u]$), and $v$ is a proper descendant of $u$ in a DFS tree.
  3. $I_u \subset I_v$, and $u$ is a proper descendant of $v$ in a DFS tree.

White-Path Theorem (CLRS Theorem 20.9): Vertex $v$ is a descendant of $u$ in the DFS forest if and only if at time $\text{tin}[u]$ when $u$ is discovered, there exists a path in $G$ from $u$ to $v$ consisting entirely of WHITE vertices.

Complete Classification of Directed & Undirected Edges

When DFS at vertex $u$ inspects an outgoing edge $(u, v)$, the color and timestamps of $v$ classify $(u, v)$ into one of four mutually exclusive categories:

  1. Tree Edge: $v$ is WHITE at inspection time. DFS recurses on $v$, making $u$ the tree parent of $v$.
  2. Back Edge: $v$ is GRAY at inspection time ($\text{tin}[v] \le \text{tin}[u]$ and $\text{tout}[v]$ is not yet assigned). Thus $v$ is an active ancestor of $u$ on the call stack. A directed graph has a cycle if and only if a DFS yields at least one back edge.
  3. Forward Edge: $v$ is BLACK at inspection time and $\text{tin}[u] < \text{tin}[v]$. Here $v$ is a proper descendant of $u$ that was already visited via another tree branch.
  4. Cross Edge: $v$ is BLACK at inspection time and $\text{tin}[v] < \text{tout}[v] < \text{tin}[u]$. The intervals $I_u$ and $I_v$ are disjoint.

Crucial Undirected Graph Theorem: In an undirected simple graph $G$, every edge is either a Tree Edge or a Back Edge. Forward and cross edges cannot exist because if ${u, v}$ is first explored from whichever endpoint is discovered earlier (say $u$), $v$ is still WHITE or GRAY; it can never be BLACK before ${u, v}$ is traversed.

DFS Edge Classification & Nested [tin, tout] Intervals A[1,8] B[2,5] C[6,7] D[3,4] ● Tree Edge (v is WHITE): discovery edge ● Back Edge (v is GRAY): cycle witness! ● Forward Edge (v is BLACK, tin[u] < tin[v]) ● Cross Edge (v is BLACK, tout[v] < tin[u])
flowchart TD Inspect["DFS at u inspects edge (u, v)"] --> Color{"State of v?"} Color -- "WHITE (0)" --> Tree["Tree Edge: recurse DFS(v)"] Color -- "GRAY (1)" --> Back["Back Edge: v is active ancestor (Cycle!)"] Color -- "BLACK (2)" --> Time{"tin[u] < tin[v]?"} Time -- "Yes" --> Fwd["Forward Edge: v is proper descendant"] Time -- "No" --> Cross["Cross Edge: disjoint subtree finished earlier"]
0-1 BFS Deque, DFS Edge Classification & Tarjan tin/low Bridges & SCCs
O(V + E) Single-Pass Traversal

Step through 0-1 BFS deque relaxation (push_front for w=0, push_back for w=1), Tarjan’s undirected bridge/articulation criterion (low[v] > tin[u] / low[v] ≥ tin[u]), and Tarjan’s single-pass SCC condensation.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.
back cross-SCC back v0 1/1 v1 2/1 v2 3/1 v3 4/4 v4 5/4 v5 6/6 ROOT
Active Tarjan Stack
[v0, v1, v2, v3, v4]
Extracted SCCs
SCC #1: {v5}
Root Invariant
low[u] == tin[u]
[Step 1] DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.
Hardware Reality — Recursion Stack Overflow & Explicit Stack Frame Conversion

Default OS thread stacks are 1 MB on Windows and 8 MB on Linux/macOS. A recursive C++ DFS frame consumes 32–64 bytes (saved registers, return address, local variables). On a degenerate path graph of $V = 2 \times 10^5$ vertices, recursive DFS exceeds 8 MB and triggers a fatal SIGSEGV stack overflow. Production graph engines replace call-stack recursion with an explicit heap-allocated std::vector<Frame> storing (u, nextEdgeIndex).

Complexity Matrix

Operation Time Complexity Space Complexity Invariant Used
Full DFS + Timestamps O(V + E) O(V) Each vertex colored WHITE→GRAY→BLACK once
Ancestor Query isAncestor(u, v) O(1) O(V) Parenthesis Theorem: $\text{tin}[u] \le \text{tin}[v]$ and $\text{tout}[v] \le \text{tout}[u]$
Directed Cycle Detection O(V + E) O(V) Existence of edge $(u, v)$ to a GRAY vertex $v$

Production Implementation: Iterative DFS with Timestamps & Cycle Reconstruction

#include <vector>
#include <algorithm>

struct DFSTimestamps {
    std::vector<int> tin, tout, parent;
    std::vector<uint8_t> color; // 0=WHITE, 1=GRAY, 2=BLACK
    std::vector<int> cycle;
    int timer = 0;

    DFSTimestamps(int n, const std::vector<std::vector<int>>& adj)
        : tin(n, 0), tout(n, 0), parent(n, -1), color(n, 0) {
        struct Frame { int u; size_t edgeIdx; };
        std::vector<Frame> st;
        st.reserve(n);

        for (int root = 0; root < n; ++root) {
            if (color[root] != 0) continue;
            color[root] = 1;
            tin[root] = ++timer;
            st.push_back({root, 0});

            while (!st.empty()) {
                auto& [u, idx] = st.back();
                if (idx < adj[u].size()) {
                    int v = adj[u][idx++];
                    if (color[v] == 0) {
                        parent[v] = u;
                        color[v] = 1;
                        tin[v] = ++timer;
                        st.push_back({v, 0});
                    } else if (color[v] == 1 && cycle.empty()) {
                        // Back edge u -> v detected; reconstruct directed cycle
                        for (int cur = u; cur != v; cur = parent[cur]) {
                            cycle.push_back(cur);
                        }
                        cycle.push_back(v);
                        std::reverse(cycle.begin(), cycle.end());
                    }
                } else {
                    color[u] = 2;
                    tout[u] = ++timer;
                    st.pop_back();
                }
            }
        }
    }

    bool isAncestor(int u, int v) const noexcept {
        return tin[u] <= tin[v] && tout[v] <= tout[u];
    }
};
Failure Mode — Naive Stack-Push Iterative DFS Corrupts Edge Classification

A common anti-pattern is writing iterative DFS by replacing std::queue in BFS with std::stack and pushing all neighbors of $u$ onto the stack at once. That pseudo-DFS marks or duplicates vertices before visiting them and fails to advance along a single path before pushing siblings, invalidating the Parenthesis Theorem and GRAY ancestor stack. Always store the per-vertex neighbor iterator (u, edgeIdx) on the stack.

Check yourself Question 1 of 4
Concept Check O(1) Ancestor Test Using DFS Timestamps recall
Section: Parenthesis Theorem

Given entry timestamps `tin[0..V-1]` and exit timestamps `tout[0..V-1]` from a DFS of a rooted tree, which boolean condition holds if and only if $u$ is an ancestor of $v$ (including $u = v$)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

A developer attempts to detect cycles in a directed graph by running DFS with a single boolean array `visited[u]` and reporting a cycle whenever an edge $(u, v)$ hits `visited[v] == true`. On which graph topology does this produce a false positive?

Key Takeaways

The Parenthesis Theorem guarantees that for any two vertices u and v, their [tin, tout] intervals are either completely disjoint or one is strictly nested inside the other.
Testing whether u is an ancestor of v in the DFS forest takes O(1) time after an O(V + E) preprocessing pass: tin[u] <= tin[v] and tout[v] <= tout[u].
A directed graph is cyclic if and only if DFS encounters a Back Edge (an edge from the current vertex u to a GRAY ancestor v on the active recursion stack).
In an undirected simple graph, every edge is classified as either a Tree Edge or a Back Edge; Forward and Cross edges never occur.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue