Graph Algorithms contents
Depth-First Search
Explore a graph as deep as possible before backtracking; entry and exit times, edge classification, and how to avoid Python's recursion limit.
Read first: Graphs: Terminology and Representation, Recursion
Depth-first search (DFS) goes as deep as possible along a path before backtracking. Where BFS uses a queue and explores by distance, DFS uses a stack (explicitly or via recursion) and explores by following one branch to its end.
Its running time is , and it is the basis of many algorithms: connected components, cycle detection, topological sorting, bridges, strongly connected components.
Algorithm
From vertex : mark it visited; for each neighbour that has not been visited, recursively run DFS from . When all neighbours are done, return.
def dfs_recursive(adj, s):
visited = [False] * len(adj)
order = []
def go(v):
visited[v] = True
order.append(v)
for u in adj[v]:
if not visited[u]:
go(u)
go(s)
return order
adj = [[1, 2], [0, 3], [0, 3], [1, 2, 4], [3]]
assert dfs_recursive(adj, 0) == [0, 1, 3, 2, 4]Iterative DFS
Use an explicit stack. The simplest version pushes all neighbours; the visit order may differ from the recursive one, but it's a valid DFS order (each vertex is visited before the vertices discovered from it):
def dfs_iterative(adj, s):
visited = [False] * len(adj)
order = []
stack = [s]
while stack:
v = stack.pop()
if visited[v]:
continue
visited[v] = True
order.append(v)
for u in reversed(adj[v]): # reversed: process neighbours in list order
if not visited[u]:
stack.append(u)
return order
assert dfs_iterative(adj, 0) == dfs_recursive(adj, 0)For algorithms that need the exit moment (post-order, timestamps, low-link values), keep an iterator index for each vertex on the stack, so the vertex stays on the stack until all its neighbours have been processed:
def dfs_times(adj, s):
"""Iterative DFS returning entry times, exit times and parents."""
n = len(adj)
tin, tout = [-1] * n, [-1] * n
parent = [-1] * n
next_i = [0] * n # next neighbour index to examine, per vertex
timer = 0
tin[s] = timer; timer += 1
stack = [s]
while stack:
v = stack[-1]
if next_i[v] < len(adj[v]):
u = adj[v][next_i[v]]
next_i[v] += 1
if tin[u] == -1:
parent[u] = v
tin[u] = timer; timer += 1
stack.append(u)
else:
tout[v] = timer; timer += 1
stack.pop()
return tin, tout, parent
tin, tout, parent = dfs_times(adj, 0)
assert (tin[0], tout[0]) == (0, 9)
assert parent == [-1, 0, 3, 1, 3] # vertex 2 is discovered from 3, not from 0Entry and exit times
Record tin[v] when DFS enters and tout[v] when it leaves. They give constant-time tests about the DFS tree:
- is an ancestor of exactly when and ;
- sorting vertices by
toutdescending gives a topological order of a DAG.
def is_ancestor(u, v):
return tin[u] <= tin[v] and tout[v] <= tout[u]
assert is_ancestor(0, 4) and is_ancestor(1, 3) and not is_ancestor(2, 3)Classification of edges
In a directed graph, during a DFS every edge is one of:
| Kind | How to recognise it when examined |
|---|---|
| tree edge | is unvisited |
| back edge | is visited and still on the stack (an ancestor): closes a cycle |
| forward edge | is a visited descendant of |
| cross edge | is visited, finished, and not a descendant |
In an undirected graph only tree edges and back edges exist. A directed graph has a cycle if and only if DFS finds a back edge; see finding a cycle.
Applications
- Connected components (article).
- Topological sorting (article).
- Cycle detection (article).
- Bridges and articulation points (bridges, articulation points).
- Strongly connected components (article).
- Subtree computations on trees: sizes, depths, sums, using the order in which DFS finishes vertices.
def subtree_sizes(adj, root):
"""Size of the subtree of every vertex of a tree, computed without recursion."""
n = len(adj)
parent = [-1] * n
order = []
stack = [root]
while stack:
v = stack.pop()
order.append(v)
for u in adj[v]:
if u != parent[v]:
parent[u] = v
stack.append(u)
size = [1] * n
for v in reversed(order): # children come after parents in `order`
if parent[v] != -1:
size[parent[v]] += size[v]
return size
tree = [[1, 2], [0, 3, 4], [0], [1], [1]]
assert subtree_sizes(tree, 0) == [5, 3, 1, 1, 1]The pattern "collect a pre-order, then process it in reverse" replaces most recursive tree DP in Python.
BFS or DFS?
| Need | Use |
|---|---|
| shortest path (unweighted) | BFS |
| any path, components, cycle, ordering, ancestors | DFS |
| the state space is very deep and narrow | DFS (memory) |
Practice problems
- SPOJ: ABCPATH
- SPOJ: EAGLE1
- Codeforces: Kefa and Park
- Timus:Werewolf
- Timus:Penguin Avia
- Timus:Two Teams
- SPOJ - Ada and Island
- UVA 657 - The die is cast
- SPOJ - Sheep
- SPOJ - Path of the Rightenous Man
- SPOJ - Validate the Maze
- SPOJ - Ghosts having Fun
- Codeforces - Underground Lab
- DevSkill - Maze Tester (archived)
- DevSkill - Tourist (archived)
- Codeforces - Anton and Tree
- Codeforces - Transformation: From A to B
- Codeforces - One Way Reform
- Codeforces - Centroids
- Codeforces - Generate a String
- Codeforces - Broken Tree
- Codeforces - Dasha and Puzzle
- Codeforces - Making genome In Berland
- Codeforces - Road Improvement
- Codeforces - Garland
- Codeforces - Labeling Cities
- Codeforces - Send the Fool Further!
- Codeforces - The tag Game
- Codeforces - Leha and Another game about graphs
- Codeforces - Shortest path problem
- Codeforces - Upgrading Tree
- Codeforces - From Y to Y
- Codeforces - Chemistry in Berland
- Codeforces - Wizards Tour
- Codeforces - Ring Road
- Codeforces - Mail Stamps
- Codeforces - Ant on the Tree
- SPOJ - Cactus
- SPOJ - Mixing Chemicals