DFS on Graphs
Trees Lied to You About DFS



Tree DFS never worries about visiting the same node twice. Parent points to child, child never points back. You recurse down, you return up, done.
Graphs break this. A graph can have cycles, and a cycle means your DFS will loop forever unless you track what you've already seen. The visited array is the single most important addition when moving from tree DFS to graph DFS.
Why Trees Don't Need Visited
In a rooted tree, every edge goes from parent to child. When you call dfs(child) from dfs(parent), the child has no way to reach the parent again through its own children. The structure prevents revisits.
In the graph on the right, starting DFS at node 1: visit 1, visit 2, visit 3, visit 4... and 4 connects back to 1. Without a visited check, you'd loop through 1-2-3-4-1-2-3-4 forever.
⚠ Gotcha. Every graph DFS must check
visitedbefore recursing. Forgetting this causes infinite loops or stack overflows. No exceptions.
The Graph DFS Template
Here's the template. Compare it to tree DFS -- the only additions are the visited array and the if (!visited[v]) guard.
bool visited[200005];
vector<int> adj[200005];
void dfs(int u) {
visited[u] = true; // mark BEFORE recursing
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v);
}
}
}
Three differences from tree DFS:
visitedarray replaces the structural guarantee of parent-child edges.- Iterate over
adj[u]instead ofleft/right-- graphs have arbitrary neighbors. - Mark visited at entry, not at the call site. This prevents double-processing.
💡 Key insight. Tree DFS has an implicit
visitedguarantee: the tree structure itself prevents revisits. Graph DFS must make that guarantee explicit.
DFS Trace on a Graph
Consider this undirected graph with 6 nodes and 7 edges:
Adjacency list (sorted neighbors):
- 1: [2, 4]
- 2: [1, 3]
- 3: [2, 5, 6]
- 4: [1, 5]
- 5: [3, 4]
- 6: [3]
DFS starting from node 1:
| Step | Current | Visited Set | Action | Stack State |
|---|---|---|---|---|
| 1 | 1 | {1} | Visit 1, try neighbor 2 | [1] |
| 2 | 2 | {1,2} | Visit 2, try neighbor 1 (visited), try 3 | [1,2] |
| 3 | 3 | {1,2,3} | Visit 3, try 2 (visited), try 5 | [1,2,3] |
| 4 | 5 | {1,2,3,5} | Visit 5, try 3 (visited), try 4 | [1,2,3,5] |
| 5 | 4 | {1,2,3,4,5} | Visit 4, try 1 (visited), try 5 (visited) | [1,2,3,5,4] |
| 6 | -- | {1,2,3,4,5} | Backtrack from 4, backtrack from 5, try 6 | [1,2,3] |
| 7 | 6 | {1,2,3,4,5,6} | Visit 6, try 3 (visited), backtrack | [1,2,3,6] |
DFS order: 1, 2, 3, 5, 4, 6.
Notice how "try 1 (visited)" at step 2 and "try 3 (visited)" at step 4 are the moments where visited prevents infinite loops. Without it, step 2 would recurse back into node 1 and we'd never finish.
Connected Components
An undirected graph might not be connected. There could be isolated clusters with no edges between them. Each cluster is a connected component.
To count components: run DFS from each unvisited node. Each DFS call explores one entire component.
First DFS from node 1 visits {1, 2, 3, 4}. Nodes 5, 6, 7 are still unvisited. Second DFS from node 5 visits {5, 6, 7}. Two components total.
Component Counting Trace
| Outer Loop i | visited[i]? | Action | Components |
|---|---|---|---|
| 1 | No | DFS(1) visits | 1 |
| 2 | Yes | Skip | 1 |
| 3 | Yes | Skip | 1 |
| 4 | Yes | Skip | 1 |
| 5 | No | DFS(5) visits | 2 |
| 6 | Yes | Skip | 2 |
| 7 | Yes | Skip | 2 |
Total components: 2
💡 Key insight. "Building roads" between disconnected components requires exactly
components - 1new edges. That's CSES Building Roads in one line of logic.
Cycle Detection in Undirected Graphs
If during DFS you visit a neighbor that is already visited and it's not your parent, you've found a cycle.
Why the parent check? In an undirected graph, if node 2 was reached from node 1, then node 1 appears in adj[2]. Seeing node 1 from node 2 doesn't mean there's a cycle -- it's just the edge you arrived on. But if you see a visited node that isn't your parent, you've found a back edge, and back edges mean cycles.
int par[200005];
int cycleStart = -1, cycleEnd = -1;
bool dfs(int u, int p) {
visited[u] = true;
par[u] = p;
for (int v : adj[u]) {
if (!visited[v]) {
if (dfs(v, u)) return true;
} else if (v != p) {
cycleStart = v;
cycleEnd = u;
return true;
}
}
return false;
}
Cycle Detection Trace
Consider this graph:
DFS from node 1 (parent = 0, meaning none):
| Step | u | parent | Neighbor v | visited[v]? | v == parent? | Action |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 2 | No | -- | Recurse into 2 |
| 2 | 2 | 1 | 1 | Yes | Yes | Skip (parent edge) |
| 3 | 2 | 1 | 3 | No | -- | Recurse into 3 |
| 4 | 3 | 2 | 2 | Yes | Yes | Skip (parent edge) |
| 5 | 3 | 2 | 4 | No | -- | Recurse into 4 |
| 6 | 4 | 3 | 1 | Yes | No | Cycle found! cycleStart=1, cycleEnd=4 |
At step 6, node 4 sees neighbor 1, which is visited but is NOT its parent (parent is 3). That's a cycle.
⚠ Gotcha. The parent check only works for simple graphs (no multi-edges). If there are two edges between nodes u and v, seeing v from u after arriving from v IS a cycle, but the parent check would skip it. For multi-edge graphs, track the edge index instead of the parent node.
Reconstructing the Cycle Path
Finding a cycle exists is easy. Printing the actual cycle -- as required by CSES Round Trip -- takes one more step.
When you detect the cycle (cycleStart = v, cycleEnd = u), the path from cycleStart to cycleEnd lives in the parent[] chain. Walk backwards from cycleEnd to cycleStart:
vector<int> cycle;
cycle.push_back(cycleStart);
for (int v = cycleEnd; v != cycleStart; v = par[v]) {
cycle.push_back(v);
}
cycle.push_back(cycleStart);
reverse(cycle.begin(), cycle.end());
For our example (cycleStart = 1, cycleEnd = 4):
| Walk Step | Current Node | parent[current] | Path So Far |
|---|---|---|---|
| Start | 4 | 3 | [1, 4] |
| 1 | 3 | 2 | [1, 4, 3] |
| 2 | 2 | 1 | [1, 4, 3, 2] |
| Stop | 1 == cycleStart | -- | [1, 4, 3, 2, 1] |
After reversing: 1 -> 2 -> 3 -> 4 -> 1. That's the cycle.
Pre-Order vs Post-Order in Graph DFS
Just like tree DFS, graph DFS has two useful moments per node: entering (pre-order) and leaving (post-order).
int timer = 0;
int tin[200005], tout[200005];
void dfs(int u, int p) {
visited[u] = true;
tin[u] = timer++; // pre-order: entering node u
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, u);
}
}
tout[u] = timer++; // post-order: leaving node u
}
- Pre-order (tin): when you first discover the node. Useful for DFS ordering, timestamps.
- Post-order (tout): when you've finished exploring all reachable nodes from it. Essential for topological sort, SCC detection, and bridge-finding.
On the graph 1-2-3-5-4-6 from our earlier trace:
| Node | tin (enter) | tout (leave) |
|---|---|---|
| 1 | 0 | 11 |
| 2 | 1 | 10 |
| 3 | 2 | 9 |
| 5 | 3 | 6 |
| 4 | 4 | 5 |
| 6 | 7 | 8 |
💡 Key insight. If
tin[u] < tin[v]andtout[u] > tout[v], then v was discovered and finished during u's DFS call. This means v is a descendant of u in the DFS tree. This ancestor/descendant test comes up repeatedly in later chapters.
The DFS Tree
When DFS explores a graph, it implicitly builds a DFS tree. The edges used to discover new nodes (the ones where !visited[v] was true) form a spanning tree of each component.
The remaining edges -- where visited[v] was already true -- are called back edges (in undirected graphs). These are exactly the edges that create cycles.
Every undirected graph has exactly one DFS tree per component (for a given starting node and neighbor ordering). The number of back edges equals the number of independent cycles.
Try It
The starter code has a DFS that detects whether a cycle exists but doesn't print it yet. Add cycle path reconstruction.
Test case 1 -- graph with a cycle:
Test case 2 -- graph without a cycle (a tree):
Challenge: What happens if the graph has multiple connected components, some with cycles and some without? Does the code handle this correctly? (Yes -- the outer loop tries each unvisited node, and the first component with a cycle will be found.)
Challenge 2: Modify the code to count the number of connected components AND detect if any component has a cycle. Print both: the component count and whether a cycle exists.
Problems
| Problem | Link | Difficulty |
|---|---|---|
| CSES Building Roads | cses.fi/problemset/task/1666 | Easy |
| CSES Round Trip | cses.fi/problemset/task/1669 | Medium |
| LC 323 — Number of Connected Components | leetcode.com/problems/number-of-connected-components-in-an-undirected-graph | Medium |
| LC 261 — Graph Valid Tree | leetcode.com/problems/graph-valid-tree | Medium |