Graph Algorithms That Show Up Everywhere (And How to Spot Them)
2026-04-20 · 7 min read
Graph problems have a reputation for being exotic. They're not. Once you learn five core primitives, you'll start seeing graph structure in problems that don't even mention "graph" in the title.
BFS: shortest path in unweighted graphs
BFS explores a graph level by level. The first time you reach a node, you've found the shortest path to it (in terms of edge count). This makes BFS the right tool for:
- Shortest path in an unweighted graph
- Finding all nodes within
khops - Multi-source BFS (start from multiple sources simultaneously)
LeetCode signal: "minimum steps to reach..." or "minimum operations to transform..."
Word Ladder is the canonical example: each word is a node, two words are connected if they differ by one letter. Find the shortest path from start to end word.
from collections import deque
def ladderLength(beginWord, endWord, wordList):
wordSet = set(wordList)
queue = deque([(beginWord, 1)])
while queue:
word, length = queue.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word == endWord:
return length + 1
if next_word in wordSet:
wordSet.remove(next_word)
queue.append((next_word, length + 1))
return 0
DFS: connectivity and cycle detection
DFS dives deep before backtracking. Use it for:
- Checking if a graph is connected
- Detecting cycles
- Topological sort (with a modification)
- Finding all paths between two nodes
LeetCode signal: "all possible paths..." or "can you reach..." or "detect cycle..."
Dijkstra: shortest path in weighted graphs
When edges have weights, BFS can't be used directly. Dijkstra uses a min-heap to always extend the cheapest known path:
import heapq
def dijkstra(graph, start):
dist = {node: float('inf') for node in graph}
dist[start] = 0
heap = [(0, start)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]:
continue
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
heapq.heappush(heap, (dist[v], v))
return dist
LeetCode signal: "minimum cost path..." with weighted edges.
Union-Find: connected components and cycle detection in undirected graphs
Union-Find (Disjoint Set Union) is the cleanest structure for grouping nodes into components and checking if two nodes are in the same component:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False # already connected — adding this edge creates a cycle
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
LeetCode signal: "number of connected components", "redundant connection", "accounts merge"
Topological Sort: ordering with dependencies
For directed acyclic graphs where you need to order nodes such that every edge points forward:
from collections import deque
def topological_sort(n, prerequisites):
in_degree = [0] * n
graph = [[] for _ in range(n)]
for a, b in prerequisites:
graph[b].append(a)
in_degree[a] += 1
queue = deque([i for i in range(n) if in_degree[i] == 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 [] # empty = cycle exists
LeetCode signal: "course schedule", "task ordering", "dependency resolution"
The meta-skill: graph modeling
The hardest part isn't the algorithm — it's recognizing that a problem is a graph problem and modeling it correctly. Ask:
- What are the nodes? (states, positions, words, people)
- What are the edges? (valid transitions, connections, relationships)
- Is the graph directed or undirected?
- Are edges weighted?
Once you've answered those four questions, the algorithm choice is usually obvious.