Expert: advanced patterns and interviews · Graphs · topic 43 of 61 · 40 min
Depth-first search (DFS)
Explanation
Go deep first, then back up
Depth-first search (DFS) picks a neighbour and keeps going deeper until it reaches a dead end, then backtracks (goes back) to the last vertex that still has an unvisited neighbour. It is the same idea as the recursion and backtracking of Module 5 and the tree traversals of Module 7, with one addition: a visited set, because a graph can lead you back to a vertex you have already seen.
def dfs(adj, u, visited):
visited.add(u)
for v in adj[u]:
if v not in visited:
dfs(adj, v, visited)
adj = {0: [1, 2], 1: [0], 2: [0], 3: [4], 4: [3]}
seen = set()
dfs(adj, 0, seen)
print(sorted(seen)) # Output: [0, 1, 2]The same search works with an explicit stack (a Python list with append and pop). This avoids Python's recursion limit. The visiting order is different from the recursive version, which does not matter when you only need to know what is reachable.
def dfs_iter(adj, start):
visited = {start}
stack = [start]
order = []
while stack:
u = stack.pop()
order.append(u)
for v in adj[u]:
if v not in visited:
visited.add(v)
stack.append(v)
return order
print(dfs_iter({0: [1, 2], 1: [3], 2: [], 3: []}, 0)) # Output: [0, 2, 1, 3]Like BFS, DFS looks at every vertex and every edge a constant number of times: O(V + E) time with an adjacency list, and O(V) extra space for the visited set and the stack (or the call stack).
Connected components and islands
To count the separate pieces of an undirected graph, loop over every vertex. Whenever you find one that is not visited yet, you have found a new component: add 1 to the count and run a DFS from it to mark its whole component. The "number of islands" problem is the same on a grid: each unvisited land cell starts a new island, and the DFS marks all land connected to it up, down, left and right.
Cycles in an undirected graph
In an undirected graph, the edge you just came along leads straight back to your parent, which is visited. That is not a cycle. So pass the parent into the DFS: if you meet a visited neighbour that is NOT your parent, you have found a cycle.
def has_cycle_undirected(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
visited = [False] * n
def dfs(u, parent):
visited[u] = True
for v in adj[u]:
if not visited[v]:
if dfs(v, u):
return True
elif v != parent:
return True
return False
return any(not visited[u] and dfs(u, -1) for u in range(n))
print(has_cycle_undirected(3, [(0, 1), (1, 2)])) # Output: False
print(has_cycle_undirected(3, [(0, 1), (1, 2), (2, 0)])) # Output: TrueCycles in a directed graph: three colours
In a directed graph, meeting a visited vertex is not enough. Take 0 → 1, 0 → 2, 1 → 3, 2 → 3 (a diamond). DFS reaches 3 through 1, finishes it, and later meets 3 again through 2. Vertex 3 is visited, but there is no cycle, because you cannot get from 3 back to 0.
The fix is three colours. White means not visited yet. Grey means "on the current path": the DFS has entered it and not yet finished it (it is on the recursion stack). Black means finished. An edge to a GREY vertex points back into the current path, so it closes a cycle. An edge to a black vertex is safe.
Recursion depth in Python
Every recursive call uses a frame on the call stack, and Python stops at a recursion limit, 1000 by default (sys.getrecursionlimit()). A path of 50,000 vertices, or a 300 × 300 grid that is all land, raises RecursionError with recursive DFS. sys.setrecursionlimit() can raise the limit, but going too high can crash the interpreter, and in the browser (Pyodide) the real stack may be smaller. On big inputs, use an explicit stack, or BFS when the order does not matter.
Common mistakes
- Using a plain visited set to find cycles in a directed graph, which reports a "cycle" on every diamond. Fix: three colours (white, grey, black).
- In an undirected graph, treating the edge back to the parent as a cycle. Fix: pass the parent and ignore it once.
- Running DFS only from vertex 0 and missing the other components. Fix: loop over every vertex and start a DFS from each unvisited one.
- Recursive DFS on a huge graph or grid, ending in RecursionError. Fix: an explicit stack.
- Changing the input grid (marking land as "0") without saying so. Fix: say it, or keep a separate visited set.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Depth-first search (DFS) · 1/8
This lesson is about depth-first search: go as deep as you can along one path, then back up and try the next branch.
Prefer a video by a teacher? Find free videos on “depth first search dfs number of islands cycle detection directed graph explained” on YouTube ↗
In real life: Counting flooded zones on a map
During heavy rain, a city's disaster-management team may split a map into a grid and mark each cell as flooded ("1") or dry ("0") from sensor or satellite data. Flooded cells that touch up, down, left or right form one zone, and each zone may need its own rescue team. Counting zones is the number-of-islands problem: scan the grid and start a DFS from every flooded cell not yet seen.
grid = ["11000",
"11010",
"00011",
"10000"]
rows, cols = len(grid), len(grid[0])
seen = set()
zones = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1" and (r, c) not in seen:
zones += 1
stack = [(r, c)]
seen.add((r, c))
while stack:
cr, cc = stack.pop()
for nr, nc in ((cr + 1, cc), (cr - 1, cc), (cr, cc + 1), (cr, cc - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "1" and (nr, nc) not in seen:
seen.add((nr, nc))
stack.append((nr, nc))
print(zones)Practice (0/4 done)
Write path_exists(n, edges, s, t) for an UNDIRECTED graph with vertices 0 to n − 1 given as a list of pairs. Return True if you can get from s to t along edges (True when s == t), else False. Use DFS with a stack or recursion. Example: path_exists(3, [(0, 1), (1, 2)], 0, 2) returns True.
Write count_components(n, edges) for an UNDIRECTED graph with vertices 0 to n − 1. Return the number of connected components (an isolated vertex is a component on its own). Example: count_components(5, [(0, 1), (1, 2), (3, 4)]) returns 2.
Write num_islands(grid). grid is a list of lists of the strings "1" (land) and "0" (water). An island is a group of land cells joined up, down, left or right (not diagonally). Return the number of islands. You may change grid. Use an EXPLICIT stack or a queue: one test is a 60 × 60 block of land, which is too deep for plain recursion in Python. An empty grid has 0 islands.
Write has_cycle_directed(n, edges) for a DIRECTED graph with vertices 0 to n − 1. edges is a list of pairs (u, v) meaning u → v. Return True if the graph has a directed cycle (a self-loop u → u counts), else False. Use DFS with three colours: white (new), grey (on the current path) and black (finished). Example: has_cycle_directed(3, [(0, 1), (1, 2), (2, 0)]) returns True.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Topological sortGraphs · same module
- Recursion: functions that call themselvesRecursion and backtracking
- Minimum spanning treesTries, union-find and bit manipulation
- Core DSA patterns in C++C++ Programming (modern C++ and DSA) · Competitive programming, interviews and careers
- Recursive CTEsSQL and Databases · Subqueries, CTEs and set operations
- Higher-order functions and recursionJavaScript Programming (language in depth) · Functions and closures
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Directed edges 0→1, 0→2, 1→3, 2→3. A DFS from 0 meets vertex 3 a second time. Is there a cycle?
Answer: No: 3 is finished (black), not on the current path
Only an edge to a grey vertex (on the current path) closes a cycle. This is a diamond.
In three-colour DFS, what does a grey vertex mean?
Answer: It is on the current path (entered, not finished)
Grey vertices are exactly the ones on the recursion stack right now.
What is Python's default recursion limit?
Answer: 1000
sys.getrecursionlimit() returns 1000 by default, so deep recursive DFS raises RecursionError.
How do you count the connected components of an undirected graph?
Answer: Start a DFS from every unvisited vertex and count the starts
Each new start finds a piece no earlier DFS could reach.
In undirected cycle detection, DFS at vertex u sees its parent p, which is visited. What does that mean?
Answer: Nothing: it is the edge you came along
The edge u–p is the one used to reach u. Only a visited neighbour other than the parent means a cycle.
An undirected graph has 5 edges. What do all the degrees add up to?Related
Answer: 10
Every edge touches two vertices, so it adds 2 to the total: 2 × 5 = 10.
Why use collections.deque instead of a list for BFS in Python?Related
Answer: popleft() is O(1), while list.pop(0) shifts every item, O(n)
Taking from the front of a list moves all other items; a deque is built for both ends.
Kahn's algorithm outputs 4 vertices of a 6-vertex graph. What does that tell you?Related
Answer: The graph has a cycle
Vertices on a cycle never reach in-degree 0, so they are left out.
From Topological sort
What does sys.getrecursionlimit() return in a fresh Python session?Related
Answer: 1000
The default limit is 1000 frames; deeper recursion raises RecursionError.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Graphs” before its exam: Topological sort, Shortest paths: Dijkstra and friends, Graph problem patterns.
- Module 9 exam: Graphs · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
- Then the next module, Tries, union-find and bit manipulation. 61 topics left in the course before the final exam.