Graph Algorithm Templates
Another cheat sheet: six self-contained, runnable graph algorithm templates, each demoed on a tiny sample graph so a paste-and-run confirms it works before you adapt it to the real problem in front of you.
What you’ll learn
Section titled “What you’ll learn”- BFS — shortest path in an unweighted graph, .
- DFS — both iterative and recursive, .
- Dijkstra — shortest paths with non-negative weights, .
- Prim — minimum spanning tree, growing one tree with a heap.
- Kruskal — minimum spanning tree, sorting edges + Union-Find.
- Kahn’s algorithm — topological sort via in-degree, with built-in cycle detection.
BFS — shortest path, unweighted graph
Section titled “BFS — shortest path, unweighted graph”Use it whenever every edge costs the same and the question is “fewest steps” or “shortest path.” The first time BFS reaches a node is guaranteed to be the shortest route. .
from collections import deque
def bfs_shortest_path(graph, start, target):
visited = {start}
queue = deque([(start, 0)]) # (node, distance so far)
while queue:
node, dist = queue.popleft()
if node == target:
return dist
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, dist + 1))
return -1 # target unreachable
graph = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]}
print("shortest 0 -> 4:", bfs_shortest_path(graph, 0, 4)) # expect 3DFS — iterative and recursive
Section titled “DFS — iterative and recursive”Use it for “does a path exist,” connected components, or flood fill — anything about whether something is reachable rather than how far. Both versions visit the same nodes/edges; iterative avoids Python’s recursion-depth limit on deep graphs.
def dfs_recursive(graph, node, visited=None, order=None):
if visited is None:
visited, order = set(), []
visited.add(node)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited, order)
return order
def dfs_iterative(graph, start):
visited = {start}
stack = [start]
order = []
while stack:
node = stack.pop()
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
return order
graph = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]}
print("DFS recursive:", dfs_recursive(graph, 0))
print("DFS iterative:", dfs_iterative(graph, 0))Dijkstra — shortest paths, non-negative weights
Section titled “Dijkstra — shortest paths, non-negative weights”Use it the moment edges carry different (but non-negative) weights — BFS’s “first arrival is shortest” guarantee no longer holds. A min-heap always expands the closest unfinalized node next. .
import heapq
from collections import defaultdict
def dijkstra(n, edges, source):
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((v, w))
graph[v].append((u, w)) # remove this line for a DIRECTED graph
dist = [float("inf")] * n
dist[source] = 0
heap = [(0, source)]
while heap:
d, node = heapq.heappop(heap)
if d > dist[node]:
continue # stale heap entry
for neighbor, weight in graph[node]:
new_dist = d + weight
if new_dist < dist[neighbor]:
dist[neighbor] = new_dist
heapq.heappush(heap, (new_dist, neighbor))
return dist
edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5), (3, 4, 3)]
print("distances from 0:", dijkstra(5, edges, 0))Prim — minimum spanning tree, grow one tree
Section titled “Prim — minimum spanning tree, grow one tree”Use it on dense graphs (or when you already have an adjacency list): start from one node and repeatedly add the cheapest edge that reaches a brand-new node. .
import heapq
from collections import defaultdict
def prim_mst(n, weighted_edges, start=0):
graph = defaultdict(list)
for u, v, w in weighted_edges:
graph[u].append((w, v))
graph[v].append((w, u))
visited = {start}
min_heap = graph[start][:]
heapq.heapify(min_heap)
total_weight = 0
while min_heap and len(visited) < n:
weight, node = heapq.heappop(min_heap)
if node in visited:
continue
visited.add(node)
total_weight += weight
for next_weight, neighbor in graph[node]:
if neighbor not in visited:
heapq.heappush(min_heap, (next_weight, neighbor))
return total_weight
weighted_edges = [(0, 1, 4), (0, 2, 1), (1, 2, 2), (1, 3, 5), (2, 4, 7), (2, 3, 8), (3, 4, 3)]
print("MST total weight:", prim_mst(5, weighted_edges)) # expect 11Kruskal — minimum spanning tree, sort + Union-Find
Section titled “Kruskal — minimum spanning tree, sort + Union-Find”Use it on sparse graphs given as a flat edge list: sort edges cheapest-first, then add each one only if its endpoints are in different Union-Find components. .
class DSU:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False
self.parent[ra] = rb
return True
def kruskal_mst(n, edges):
"""edges: list of (weight, u, v)."""
dsu = DSU(n)
total_weight = 0
for weight, u, v in sorted(edges):
if dsu.union(u, v):
total_weight += weight
return total_weight
edges = [(4, 0, 1), (1, 0, 2), (2, 1, 2), (5, 1, 3), (7, 2, 4), (8, 2, 3), (3, 3, 4)]
print("MST total weight:", kruskal_mst(5, edges)) # expect 11Kahn’s algorithm — topological sort
Section titled “Kahn’s algorithm — topological sort”Use it to order tasks/courses/builds by dependency, on a directed acyclic
graph. A BFS driven by in-degree: process every node with no remaining
prerequisites, and check len(order) != n for a cycle. .
from collections import deque, defaultdict
def topological_sort(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges: # u must come before v
graph[u].append(v)
in_degree[v] += 1
queue = deque(node for node in range(n) if in_degree[node] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == n else None # None means a cycle exists
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("topological order:", topological_sort(5, edges))
cyclic_edges = [(0, 1), (1, 2), (2, 0)]
print("cyclic graph result:", topological_sort(3, cyclic_edges))Complexity at a glance
Section titled “Complexity at a glance”| Algorithm | Time | Use case |
|---|---|---|
| BFS | Shortest path, unweighted | |
| DFS | Reachability, components, flood fill | |
| Dijkstra | Shortest path, non-negative weights | |
| Prim | MST, dense graph / adjacency list | |
| Kruskal | MST, sparse graph / edge list | |
| Kahn’s | Topological order + cycle detection |
Practice
Section titled “Practice”Drill 1 — BFS visited-on-enqueue. Complete the line that marks a neighbor visited the moment it’s discovered, not when it’s processed.
Drill 2 — Dijkstra’s stale-entry skip. Complete the check that skips a heap entry whose distance is already beaten.
Kahn’s algorithm — topological order, or [] if a cycle exists
Section titled “Kahn’s algorithm — topological order, or [] if a cycle exists”Pitfalls
Section titled “Pitfalls”- Marking a node visited on dequeue instead of enqueue in BFS. A node discovered by several neighbours is then pushed several times before being processed — duplicated output and a queue that grows toward . Mark when you push.
- Enqueueing a neighbour in Kahn’s on first sight. It must wait until its in-degree reaches 0, otherwise a node with two prerequisites is emitted before one of them.
- Reading Dijkstra’s stale heap entries as a bug.
heapqhas no decrease-key, so an improvement pushes a second entry;if d > dist[node]: continuediscards the obsolete one. That guard is required, not optional. - Running Dijkstra with a negative edge. It finalises a node on pop, so a negative edge found later cannot fix it — a confidently wrong answer. Negative weights are Bellman-Ford.
- Using a
visitedset in 0-1 BFS. A 0-weight edge can improve a node after it has been popped, so the correct guard isdist[v] + w < dist[u]. - Applying the directed cycle-detection method to an undirected graph. Every edge appears in both adjacency lists, so each one looks like a 2-cycle. Undirected needs the parent passed down; directed needs three colours.
- Prim’s without a
visitedcheck on pop. The heap holds stale entries just as Dijkstra’s does, and re-adding a node already in the tree inflates the MST weight. - Recursive DFS on a -node graph. CPython’s ~1000-frame limit. Use the iterative form in these templates, or raise the limit deliberately.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “BFS or DFS here?” | Judgement | Shortest path in an unweighted graph or per-level output → BFS. Reachability, components, cycles, or anything about a node’s descendants → DFS. If either works, DFS is usually shorter and uses rather than space |
| “Why does Dijkstra need non-negative weights?” | The precondition | Because it finalises a node the moment it is popped, on the assumption that nothing cheaper can arrive later. A negative edge breaks that and nothing revisits the node |
| “Kruskal or Prim?” | Structural difference | Kruskal sorts all edges and unions with DSU — better on sparse graphs and trivially parallel to reason about. Prim grows one tree with a heap — better on dense graphs. Both in practice |
| “How does Kahn’s detect a cycle?” | Reading its output | If fewer than n nodes are emitted, the remainder never reached in-degree 0 — they are on a cycle or downstream of one. len(order) != n is the entire test, and the partial order names the healthy prefix |
| “All weights are 1. Still Dijkstra?” | Not over-reaching | No — BFS is with no heap. Weights of exactly 0 and 1 want a deque. The heap only earns its log factor for arbitrary weights |
| “Detect a negative cycle” | Bellman-Ford’s extra round | Run relaxation rounds, then one more: if anything still improves, a negative cycle is reachable from the source |
| “The graph is a DAG and you want the longest path” | Why DAGs are special | Relax along edges in topological order — every predecessor is final before it is used, so longest-path becomes despite being NP-hard in general |
| ” nodes, recursive DFS” | Python awareness | It dies at the frame limit. Convert to the explicit-stack form, and say so before being asked |
LeetCode problem set
Section titled “LeetCode problem set”One ladder across all six graph templates. Sorting these by which template they need is a faster exercise than solving them, and it is the skill the interview actually tests.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 112Path Sumeasy
- 200Number of Islandsmedium
- 102Binary Tree Level Order Traversalmedium
- 207Course Schedulemedium
- 133Clone Graphmedium
- 743Network Delay Timemedium
- 787Cheapest Flights Within K Stopsmedium
- 994Rotting Orangesmedium
- 1584Min Cost to Connect All Pointsmedium
- 128Longest Consecutive Sequencemedium
- 210Course Schedule IImedium
- 261Graph Valid Treepremiummedium
- 310Minimum Height Treesmedium
- 399Evaluate Divisionmedium
- 433Minimum Genetic Mutationmedium
- 547Number of Provincesmedium
- 684Redundant Connectionmedium
- 695Max Area of Islandmedium
- 721Accounts Mergemedium
- 909Snakes and Laddersmedium
- 990Satisfiability of Equality Equationsmedium
- 1091Shortest Path in Binary Matrixmedium
- 1135Connecting Cities With Minimum Costpremiummedium
- 1136Parallel Coursespremiummedium
- 1319Number of Operations to Make Network Connectedmedium
- 1334Find the City With the Smallest Number of Neighbors at a Threshold Distancemedium
- 1631Path With Minimum Effortmedium
- 127Word Ladderhard