Structs & Data Structures · intermediate · ~14 min
Explore as deep as possible, then backtrack.
Depth-first search dives down one path until it can go no further, then backtracks and tries the next option. Implemented recursively, it visits a vertex, marks it, and calls itself on each not-yet-visited neighbour. A visited array is what keeps it from looping forever when the graph has cycles.
DFS is the backbone of graph reachability: 'which components can this failure spread to?', 'is there any dependency path from A to B?', 'can this state reach a winning state?'. It also underlies cycle detection, topological sort, and connected-components — all later lessons in this track.
Visited set. One boolean per vertex; mark on entry, never revisit.
Reachable set. The vertices DFS marks starting from src are exactly those reachable from src (including src itself).
Recursion = an implicit stack. Each call frame remembers where to resume; an explicit stack gives the same traversal iteratively.
Directed edges. Follow only i→j; do not treat the matrix as symmetric unless the graph is undirected.
#include <stdlib.h>
/* recursive DFS - mark BEFORE recursing */
static void dfs(const int *adj, int n, int v, int *vis) {
vis[v] = 1; /* mark first, or cycles loop forever */
for (int u = 0; u < n; u++)
if (adj[v*n + u] && !vis[u])
dfs(adj, n, u, vis);
}
int reachable_count(const int *adj, int n, int src) {
int *vis = calloc((size_t)n, sizeof *vis); /* zeroed = nothing visited yet */
if (!vis) return -1;
dfs(adj, n, src, vis);
int c = 0;
for (int i = 0; i < n; i++) c += vis[i]; /* src itself is included */
free(vis);
return c;
}
Key points:
vis[v] = 1 must happen before the neighbour loop; marking afterwards lets a cycle recurse forever.calloc gives the all-unvisited starting state for free.Depth-first search follows one path as far as it goes before backing up. A visited array stops it from looping forever on cycles. DFS answers the fundamental questions of reachability: which vertices can I get to from here, and is there any path from A to B? It is naturally recursive — visit a vertex, then recurse into each unvisited neighbour.
#include <stdio.h> static int n=5; static int adj[25]={ 0,1,1,0,0, 0,0,0,1,0, 0,0,0,1,0, 0,0,0,0,0, 0,0,0,0,0 }; static void dfs(int v,int vis){ vis[v]=1; for(int u=0;u<n;u++) if(adj[vn+u]&&!vis[u]) dfs(u,vis); } int main(void){ for(int s=0;s<n;s++){ int vis[5]={0}; dfs(s,vis); int cnt=0; for(int i=0;i<n;i++) cnt+=vis[i]; printf("from %d: reachable=%d can reach 3? %s\n", s, cnt, vis[3]?"yes":"no"); } return 0; }
| Step | Line | What happens |
|---|---|---|
| 1 | dfs(adj,n,0,vis) |
Marks vertex 0 visited immediately. |
| 2 | neighbour loop | Finds edge 0->1; vis[1] is 0, so it recurses into 1 before ever looking at 0->2. |
| 3 | deeper | Vertex 1 marks itself and dives into its first unvisited neighbour — depth first, not breadth. |
| 4 | dead end | When a vertex has no unvisited neighbours the call returns and control resumes in its parent's loop. |
| 5 | back at 0 | The loop continues to edge 0->2 and explores that branch. |
| 6 | count | Summing vis gives the size of the reachable set — 4 from vertex 0 in the demo graph. |
Forgetting to mark a vertex visited before recursing, which loops forever on a cycle. Treating directed edges as undirected. Not counting src itself in the reachable set.
Compiler errors and warnings:
-Wunused-parameter if you forget to pass n through to the recursion.vis mark — it manifests only at runtime.Runtime symptoms:
!vis[u] test is missing before the recursive call.adj[v][u] only.Technique: print the vertex on entry to dfs. The order shows you immediately whether the traversal is going deep (correct) or wide, and repeated vertices reveal a missing mark.
n frames; for large graphs convert to an explicit stack, which also gives you control over the memory used.vis must be exactly n entries and zeroed. calloc is correct here; malloc leaves garbage that reads as "already visited", silently truncating the traversal.src. vis[src] with an out-of-range source writes outside the allocation.const int *adj documents that the traversal does not modify the graph — useful when a caller shares one matrix across several searches.Concrete uses: Detecting whether a dependency can be reached from a root, garbage-collection mark phases, maze and puzzle solving, topological ordering, finding connected components, and cycle detection all build on DFS. Compilers use it for control-flow analysis; package managers use it to resolve dependency closures.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) Reachable count. Implement int reachable_count(const int *adj, int n, int src) including the source. Example: in the demo graph, 4 from vertex 0. Concepts: visited array, marking on entry.
2. (Beginner) Path existence. Implement int has_path(const int *adj, int n, int src, int dst) returning 1 when src == dst. Concepts: early exit, reflexive reachability.
3. (Intermediate) Iterative DFS. Rewrite the traversal with an explicit stack array and confirm it visits the same set. Concepts: removing recursion depth limits.
4. (Intermediate) Record the order. Output the vertices in the order DFS first visits them, and explain why it differs from BFS order. Concepts: traversal order.
Depth-first search follows one path as far as it goes, then backtracks, and a visited array is what keeps it from looping forever on a cycle — the mark must be set on entry, before the neighbour loop, or a cyclic graph recurses until the stack is exhausted. The set of vertices DFS marks starting from a source is exactly that source's reachable set, and it always includes the source itself. Allocate the visited array with calloc so "unvisited" is the default, validate the source index, and remember that recursion depth can reach the vertex count — use an explicit stack for large graphs, and use BFS instead when you need shortest distances rather than mere reachability.