3.5.5 · D4Graphs

Exercises — DFS — algorithm, stack - recursion, O(V+E), visited array

3,379 words15 min readBack to topic

Before we start, one shared convention so no symbol appears unexplained:


Level 1 — Recognition

(Can you read a graph and produce the DFS order by hand?)

Recall Solution L1.1

Walk the rule "mark on arrival, then descend into the first unvisited neighbor."

  • Arrive 0, mark it. Order so far: 0. First neighbor is 1, unvisited → go in.
  • Arrive 1, mark. Order: 0,1. 1's only neighbor is 3, unvisited → go in.
  • Arrive 3, mark. Order: 0,1,3. 3 has no neighbors → dead end, backtrack to 1.
  • 1 has no more neighbors → backtrack to 0.
  • 0's next neighbor is 2, unvisited → go in. Arrive 2, mark. Order: 0,1,3,2. DFS order = 0, 1, 3, 2. Notice it is not 0,1,2,3 — that "finish a level before going deeper" pattern is BFS, not DFS.
Recall Solution L1.2
  • Arrive 0, mark → 0. First neighbor 1, unvisited → in. Arrive 1, dead end, back to 0.
  • Next neighbor 2 → in, dead end, back to 0.
  • Next neighbor 3 → in, dead end. DFS order = 0, 1, 2, 3. With no deeper structure, DFS just walks the neighbor list of the root.

Level 2 — Application

(Apply the visited rule and the complexity formula to concrete numbers.)

Recall Solution L2.1

Figure — DFS — algorithm, stack - recursion, O(V+E), visited array
The figure draws the triangle: the two grey solid arrows (0→1, 1→2) are the edges DFS descends, and the coral dashed arrow (2→0) is the edge DFS tries but rejects because 0 is already marked. Read the steps against it:

  • Arrive 0, mark → 0. Neighbor 1 unvisited → in (follow the first grey arrow).
  • Arrive 1, mark → 0,1. Neighbor 2 unvisited → in (second grey arrow).
  • Arrive 2, mark → 0,1,2. 2's neighbor is 0, but visited[0] is Trueskip (the coral dashed arrow in the figure). This is the exact line where the visited array stops an infinite loop.
  • Backtrack all the way out. DFS order = 0, 1, 2. Work: vertices marked once, directed edges scanned once → basic operations.
Recall Solution L2.2

In every distinct pair of vertices is joined by exactly one edge, so counting edges = counting pairs. To count pairs: vertex 0 pairs with the other , vertex 1 with , and so on — that's ordered picks, but pair {0,1} and {1,0} are the same edge, so we divide by : (This " choose " count is written — the symbol just means "the number of ways to pick an unordered pair out of things," which is exactly the number of possible edges. We derived it above rather than quoting it.) DFS cost basic operations (each treated as one unit). In big-O this is ; here numerically 15.

Recall Solution L2.3

The Handshake Lemma says every undirected edge adds to two endpoints' degrees, so . 6 edges. Now the raw operation count. Two separate piles of work:

  • Vertex-marks: each of the vertices is marked once → operations.
  • Edge-inspections: DFS walks each undirected edge from both endpoints, so it does inspections (not ). Raw total operations. In asymptotic notation this is still , because the factor of on the edge term is a constant the big-O bound absorbs — but the honest raw count is 18, not . (Writing "" conflates the asymptotic with the raw ; they differ by that constant factor.)

Level 3 — Analysis

(Trace subtle behaviour and explain why the algorithm does what it does.)

Recall Solution L3.1

Figure — DFS — algorithm, stack - recursion, O(V+E), visited array
The figure is a diamond: 0 at the top branches to 1 (left) and 2 (right), and both 1 and 2 (the coral arrows) point down to the shared bottom vertex 3. That shared target is exactly why 3 can be reached twice. Keep an eye on it while tracing the stack.

Recall the iterative loop: pop a node; if already visited, continue; else mark, process, then push each unvisited neighbor. A stack is LIFO — the last pushed is the first popped. The side panel in the figure lists the resulting pop order.

  • Stack [0]. Pop 0, mark, process → order 0. Push its neighbors 1, then 2. Stack [1,2].
  • Pop 2 (top of stack!), mark, process → 0,2. Push its unvisited neighbor 3. Stack [1,3].
  • Pop 3, mark, process → 0,2,3. No unvisited neighbors. Stack [1].
  • Pop 1, mark, process → 0,2,3,1. Its neighbor 3 is already visited → not pushed. Stack []. Processing order = 0, 2, 3, 1.

Why 3 could have been pushed twice: both 1 and 2 list 3 as a neighbor (the two coral arrows). If 1 had been popped before 3 was visited, we'd push 3 a second time — and the stack would briefly hold two copies of 3. The if visited[u]: continue check on pop is what discards the stale copy. (In this exact ordering 3 was already visited before 1 was popped, so the second push never happened — but the check must still be there for graphs where it does.)

Recall Solution L3.2

DFS does two separate, additive chunks of work:

  1. Mark each vertex once → units.
  2. Scan each edge a constant number of times total → units. These add, they don't multiply, because scanning one vertex's neighbors doesn't re-cost the other vertices — the shared visited array guarantees each vertex/edge is touched a bounded number of times overall. Hence . A multiplicative would wrongly claim you redo all edge work for every vertex.

For , :

  • — you still must visit and mark all 1000 vertices. Honest.
  • — claims zero work, which is nonsense; you obviously still touch every isolated node. This is exactly why both terms are kept: the term protects the isolated-vertex case.

Level 4 — Synthesis

(Combine DFS with another idea to build a small algorithm.)

Recall Solution L4.1

Figure — DFS — algorithm, stack - recursion, O(V+E), visited array
The figure shows the graph split into four visually separate clusters, each shaded a different pastel: {0,1,2} joined by solid lines, {3,4} joined by one line, and the two lone dots 5 and 6 sitting alone. Each separate cluster is exactly one connected component — the figure is the answer, and the loop below just discovers these clusters one launch at a time.

Loop u from 0 to 6; each time u is unvisited, that's a brand-new component — launch DFS and increment the count.

  • u=0 unvisited → launch. DFS reaches 0,1,2. Component #1 = {0,1,2}.
  • u=1,2 now visited → skip.
  • u=3 unvisited → launch. DFS reaches 3,4. Component #2 = {3,4}.
  • u=4 visited → skip.
  • u=5 unvisited → launch. DFS reaches just 5. Component #3 = {5}.
  • u=6 unvisited → launch. DFS reaches just 6. Component #4 = {6}. Answer: 4 connected components. Cost: the shared visited array spans all launches, so across every launch combined each vertex is marked once ( total) and each edge scanned a bounded number of times ( total). Total = , unchanged — the outer loop does not multiply the cost.
Recall Solution L4.2

Keep two states beyond plain visited: mark a node on-stack (grey) while it is an active ancestor in the current recursion, and done (black) after we finish it. A cycle exists exactly when DFS finds an edge pointing to a node that is currently on-stack.

  • explore(0): mark 0 on-stack. Edge 0→1 → recurse.
  • explore(1): mark 1 on-stack. Edge 1→2 → recurse.
  • explore(2): mark 2 on-stack. Edge 2→00 is on-stackcycle detected! The revealing edge is 2→0 (a "back edge" to an active ancestor). This is why plain visited (which the parent used to prevent looping) is not enough for cycle detection — you must distinguish "seen before" from "seen and still on the active path."

Level 5 — Mastery

(Full open-ended reasoning: prove/derive, handle every case.)

Recall Solution L5.1

Post-order = record a vertex when we finish it (after all its descendants are done). Reverse that list. Trace (neighbors in listed order, mark on arrival):

  • explore(0): go to 1. explore(1): go to 3. explore(3): no neighbors → finish 3. Post: [3].
  • Back in 1: done → finish 1. Post: [3,1].
  • Back in 0: next neighbor 2. explore(2): neighbor 3 already visited → skip → finish 2. Post: [3,1,2].
  • Back in 0: done → finish 0. Post: [3,1,2,0]. Reverse post-order = 0, 2, 1, 3. Check every edge points left-to-right in this order: 0→1 ✓, 0→2 ✓, 1→3 ✓, 2→3 ✓. Valid topological order. Why reversal works (for any DAG): when DFS finishes a vertex u, every vertex reachable from u is already finished (they were explored and popped before u). So in post-order, u appears after all its descendants. Reversing puts u before all its descendants — which is exactly the topological requirement "each vertex before everything it points to." A DAG has no cycle, so no back edge ever forces a contradiction.
Recall Solution L5.2

Vertex work. The visited array blocks re-entry, so explore runs at most once per vertex. Marking + processing is each → summing over the (at most ) explored vertices gives . Edge work. Inside the single explore(u) call, we scan u's adjacency list once: inspections. Summed over all vertices that get explored: The reason this sum equals : every directed edge leaves exactly one tail vertex u, so it is counted exactly once inside that tail's explore — no edge is double-counted, none is missed. Hence edge work is . Total . These add (not multiply) because the two sums range independently — the vertex sum runs over dots, the edge sum over lines — and the visited array bounds each contribution to a constant number of touches. Degenerate cases:

  • (all vertices isolated): the edge sum is , but we still mark each reachable vertex → cost , correctly captured by the term (an claim would wrongly say "no work").
  • Single vertex 0:[0] (a self-loop): , . explore(0) marks 0, scans its one edge 0→0, sees visited[0]=True, and skips — no recursion. Work . Crucially, no infinite loop, because the self-edge hits the visited guard the instant it is followed. This completes the proof for all three cases (general, edgeless, self-loop).

Connections

  • DFS — parent note — the algorithm, complexity derivation, and code these exercises drill.
  • BFS — Breadth-First Search — contrast the level-order (queue) sibling with DFS's deep-first order.
  • Stack data structure · Recursion and the Call Stack — the machinery behind L3.1.
  • Connected Components — L4.1. · Cycle Detection — L4.2. · Topological Sort — L5.1.
  • Handshake Lemma — L2.3, why .
  • 🇮🇳 Hinglish version