Tutorials › Graph Theory (Undergraduate) › DFS Trace Workshop: Timestamps and Edge Types

Graph Traversal: BFS and DFS · Tutorial 726 of 799

DFS Trace Workshop: Timestamps and Edge Types

Learn to produce and verify complete DFS traces, including timestamps, forest edges and directed or undirected edge types.

Intermediate 12 min read

What You'll Learn

  • Record discovery and finish events with a single DFS clock.
  • Build a DFS forest using a stated root and adjacency order.
  • Classify directed edges as tree, back, forward or cross edges.
  • Classify undirected edges as tree or back edges, handling duplicate scans.
  • Use timestamp intervals to check a directed edge classification.
  • Recognize common errors caused by confusing a black endpoint with a cross edge.

How to Read a DFS Trace

A depth-first search (DFS) follows one path as far as possible before returning to explore another. Its output depends on the order in which roots and neighbors are considered, so a trace is not reproducible unless those orders are stated. This workshop fixes those choices, records the search forest and timestamps, and classifies every edge.

Graphs are finite and simple. A directed graph has ordered edges, called arcs; an undirected graph has unordered edges. We use a recursive DFS: when a vertex is first reached, mark it discovered, then scan its neighbors in the stated order. If a neighbor has not been discovered, recursively search from it and record the edge as a tree edge. Once all neighbors have been scanned, mark the vertex finished. When a search from one root finishes, choose the next undiscovered root in the stated root order. The resulting collection of tree edges is the DFS forest.

Definition: A DFS discovery time \(d(v)\) is the clock value when \(v\) is first discovered; its finish time \(f(v)\) is the value when its scan is complete. Start the clock at \(0\), increase it by \(1\) at each discovery and finish event, and do not increase it when an edge is scanned. Thus each vertex has \(d(v)<f(v)\), and all event times are distinct.

For a directed graph, each arc receives one of four labels. A tree edge discovers its endpoint. A back edge points to a currently active vertex, which is an ancestor in the recursive call stack. A forward edge points to a descendant already discovered by a different tree edge. A cross edge joins vertices for which neither is an ancestor of the other. For an undirected graph, each edge is considered once: it is either a tree edge or a back edge joining a vertex to an ancestor. In an adjacency list an undirected edge appears at both endpoints; the second scan does not give it a new classification.

Timestamp Intervals and Edge Types

The interval \([d(v),f(v)]\) records the period in which \(v\) is active in the recursive search. If \(v\) is an ancestor of \(u\), then \(v\) is discovered before \(u\), and it finishes after \(u\). Their intervals are nested. If neither vertex is an ancestor of the other, their search calls finish one after the other, so their intervals are disjoint.

Theorem: For an arc \(uv\) in a directed graph, the intervals determine its type as follows:
  • \(d(u)<d(v)<f(v)<f(u)\): \(v\) is a descendant of \(u\), so \(uv\) is a tree or forward edge.
  • \(d(v)<d(u)<f(u)<f(v)\): \(v\) is an ancestor of \(u\), so \(uv\) is a back edge.
  • \(d(v)<f(v)<d(u)<f(u)\), or the same inequalities with \(u\) and \(v\) exchanged: the intervals are disjoint, so \(uv\) is a cross edge.
The timestamps distinguish tree from forward edges only when combined with the search record: the edge is a tree edge exactly when it discovered \(v\).

Proof. Consider the moment when the search scans \(uv\). The vertex \(u\) is active, so \(d(u)<f(u)\) and \(f(u)\) has not yet occurred. If \(v\) is undiscovered, this scan discovers it; the recursive call from \(u\) finishes only after the call from \(v\), giving \(d(u)<d(v)<f(v)<f(u)\), and \(uv\) is a tree edge. If \(v\) is active, it is on the current recursion stack. The stack consists of the active ancestors of \(u\), so \(d(v)<d(u)<f(u)<f(v)\), and the arc is back. If \(v\) is finished, it cannot be an active ancestor. If it is a descendant of \(u\), its interval lies inside \(u\)'s interval, and the arc is forward unless it was the edge that discovered \(v\). Otherwise the two vertices lie in separate completed search calls, so their intervals are disjoint and the arc is cross. These cases cover every possible state of \(v\) when the arc is scanned. \(\square\)

Theorem: In an undirected graph, every non-tree edge joins a vertex to one of its ancestors in the DFS forest. In particular, there are no cross edges.

Proof. Let \(uv\) be an edge whose endpoints lie in different branches, so neither is an ancestor of the other. Whichever endpoint is discovered first, say \(u\), is still active when its adjacency list is scanned. At that time \(v\) cannot already be finished: the recursive search from \(u\) has not finished, and a separate branch containing \(v\) cannot have been completed inside that unfinished call without making \(v\) a descendant of \(u\). Thus \(v\) is undiscovered when \(uv\) is scanned, and DFS would discover \(v\) through \(uv\), making it a tree edge. This contradicts the assumption that it is a non-tree edge. Hence every non-tree edge joins an ancestor and a descendant. \(\square\)

Directed DFS: A Complete Trace

Worked Example: All Four Directed Edge Types

Let the directed graph have \(V=\{A,B,C,D\}\) and arcs \(E=\{AB,AC,AD,BD,DA,CB,CD\}\). Use root order \(A,B,C,D\), and scan outgoing neighbors in the order shown: \(A:(B,C,D)\), \(B:(D)\), \(C:(B,D)\), \(D:(A)\).

Start the clock at \(0\). The event trace is:

  • Discover \(A\): clock \(1\). Scan \(AB\); \(B\) is undiscovered, so \(AB\) is a tree edge.
  • Discover \(B\): clock \(2\). Scan \(BD\); \(D\) is undiscovered, so \(BD\) is a tree edge.
  • Discover \(D\): clock \(3\). Scan \(DA\); \(A\) is active, so \(DA\) is a back edge. Finish \(D\): clock \(4\). Finish \(B\): clock \(5\).
  • Return to \(A\). Scan \(AC\); \(C\) is undiscovered, so \(AC\) is a tree edge. Discover \(C\): clock \(6\).
  • Scan \(CB\): \(B\) is finished. Its interval is disjoint from \(C\)'s, so \(CB\) is cross. Scan \(CD\): \(D\) is also finished in a disjoint interval, so \(CD\) is cross. Finish \(C\): clock \(7\).
  • Return to \(A\). Scan \(AD\): \(D\) is a finished descendant of \(A\), so \(AD\) is forward. Finish \(A\): clock \(8\).

The search forest has one tree, rooted at \(A\), with tree edges \(AB,BD,AC\). The intervals are \(A:[1,8]\), \(B:[2,5]\), \(D:[3,4]\), and \(C:[6,7]\). The edge \(AD\) is forward because \(1<3<4<8\), while \(DA\) is back because \(1<3<4<8\) when read as \(d(A)<d(D)<f(D)<f(A)\). The cross edges satisfy \(2<5<6<7\) for \(CB\), and \(3<4<6<7\) for \(CD\).

Check: The seven arcs are accounted for: three tree edges \(AB,BD,AC\), one back edge \(DA\), one forward edge \(AD\), and two cross edges \(CB,CD\). This totals \(3+1+1+2=7=|E|\).

Notice that an endpoint being finished does not by itself make an arc cross. The interval test matters: a finished descendant gives a forward edge, while a finished vertex in a separate branch gives a cross edge.

Undirected DFS: Count Each Edge Once

Worked Example: Tree Edges and Back Edges

Let the undirected graph have \(V=\{1,2,3,4,5\}\) and \(E=\{12,13,23,24,34,45\}\). Use root order \(1,2,3,4,5\), with neighbor order \(1:(2,3)\), \(2:(1,3,4)\), \(3:(1,2,4)\), \(4:(2,3,5)\), \(5:(4)\).

The search starts by discovering \(1\) at time \(1\). Its edge \(12\) discovers \(2\) at time \(2\). At \(2\), edge \(21\) is the already-used parent edge; \(23\) discovers \(3\) at time \(3\). At \(3\), edge \(31\) joins \(3\) to the active ancestor \(1\), so \(13\) is a back edge. Edge \(32\) is the parent edge. Edge \(34\) discovers \(4\) at time \(4\). At \(4\), edge \(42\) joins \(4\) to the active ancestor \(2\), so \(24\) is a back edge; edge \(43\) is its parent edge. Edge \(45\) discovers \(5\) at time \(5\). Vertex \(5\) scans its parent edge \(54\) and finishes at time \(6\); then \(4\) finishes at \(7\), \(3\) at \(8\), \(2\) at \(9\), and \(1\) at \(10\).

The DFS tree edges are \(12,23,34,45\), and the non-tree edges are \(13,24\), both back edges. When \(2\) later scans \(24\), vertex \(4\) is finished; this is the second adjacency-list occurrence of the same undirected edge, already classified as back. Likewise, \(1\)'s later scan of \(13\) does not create a new edge classification.

Check: The tree contains four edges, one for each vertex other than the root. The two remaining edges \(13\) and \(24\) are the two back edges, so \(4+2=6=|E|\). The cycles \(1,2,3,1\) and \(2,3,4,2\) also confirm that the two non-tree edges close cycles to ancestors.

Disconnected Graphs and Search Forests

A DFS started at one vertex only visits vertices reachable from that root by directed paths in a directed graph, or paths in an undirected graph. To search every vertex, the outer loop must start a fresh DFS at each still-undiscovered vertex. Each fresh start adds a root to the DFS forest and receives timestamps from the same continuing clock.

Worked Example: Multiple Roots and an Isolated Vertex

Let the directed graph have \(V=\{p,q,r,s,t\}\) and arcs \(E=\{pq,qp,rs\}\). Use root order \(p,q,r,s,t\), and scan each vertex's outgoing neighbors in the listed order. Thus \(p:(q)\), \(q:(p)\), \(r:(s)\), and \(s,t\) have no outgoing arcs.

Discover \(p\) at time \(1\). Arc \(pq\) discovers \(q\) at time \(2\); \(qp\) points to the active ancestor \(p\), so it is back. Finish \(q\) at \(3\), then \(p\) at \(4\). The next undiscovered root is \(r\), discovered at \(5\); arc \(rs\) discovers \(s\) at \(6\). Finish \(s\) at \(7\), then \(r\) at \(8\). Vertex \(t\) is still undiscovered, so it becomes a third root: discover it at \(9\) and finish it at \(10\).

The forest roots are \(p,r,t\), and its tree edges are \(pq\) and \(rs\). Arc \(qp\) is back. The intervals are \(p:[1,4]\), \(q:[2,3]\), \(r:[5,8]\), \(s:[6,7]\), and \(t:[9,10]\).

Check: The three roots account for all five vertices: the first tree has two vertices, the second has two, and the isolated search root has one, giving \(2+2+1=5\). The two tree arcs and one back arc account for all \(3\) arcs.

Common Pitfalls and a Reliable Checklist

Wrong calculation: In the first example, one might call \(AD\) a cross edge because \(D\) is black when \(A\) scans it. That ignores the timestamps. Since \(d(A)=1\), \(d(D)=3\), \(f(D)=4\), and \(f(A)=8\), the strict nesting \(1<3<4<8\) shows that \(D\) is a descendant of \(A\); \(AD\) is forward, not cross. The search record confirms it was not the edge that discovered \(D\).

Other errors often come from changing the convention mid-trace. State the root order and neighbor order first. Advance the clock only at discovery and finish events, not at edge scans. In an undirected graph, classify each edge once rather than counting both appearances in adjacency lists. Finally, distinguish the search forest from the whole graph: only the edges that discover new vertices belong to the forest.

For a final audit, check that every vertex has exactly one discovery and one finish time, every tree edge discovers one new vertex, and every graph edge or arc has exactly one classification. In a directed graph, use interval nesting or disjointness to verify the non-tree labels. In an undirected graph, verify that each non-tree edge joins a vertex to an ancestor.

Check Your Understanding

For each question, state any root and neighbor order you use, and show the relevant trace or argument.

  1. For the directed graph \(V=\{a,b,c\}\), \(E=\{ab,ac,bc\}\), trace DFS from \(a\) with neighbor order \(b,c\). Give all timestamps and classify every arc.
  2. Why does a directed arc to a black vertex require an interval check before it can be called forward or cross?
  3. In an undirected DFS forest with \(n\) vertices and \(k\) roots, how many tree edges are there? Explain using the number of non-root vertices.
  4. Give an undirected graph and neighbor order for which a non-tree edge is first seen from its descendant endpoint. Explain why the later scan from its ancestor does not change its classification.
  5. Explain why an isolated vertex in a complete DFS search is a root of its own one-vertex tree.