Reference patterns for graph algorithm problems including BFS, DFS, Dijkstra, topological sort, union-find, MST, Bellman-Ford, and bipartite checking. Provides recognition signals, Python templates, edge cases, and common mistakes for each technique. Use when solving problems involving graphs, trees, shortest paths, connectivity, or dependency ordering.
Provides graph algorithm templates and pattern recognition for solving coding problems involving paths, connectivity, and ordering.
/plugin marketplace add sequenzia/agent-alchemy/plugin install agent-alchemy-cs-tools@agent-alchemyThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Graph problems appear frequently in competitive programming and technical interviews. The key challenge is recognizing which technique fits the problem structure. This reference covers eight core patterns with recognition heuristics, templates, and pitfall guides.
| Trigger Signals | Technique | Typical Complexity |
|---|---|---|
| Shortest path, unweighted, fewest steps | BFS | O(V + E) |
| Explore all paths, connected components, backtracking | DFS | O(V + E) |
| Shortest path, weighted (non-negative) | Dijkstra | O((V + E) log V) |
| Dependencies, ordering, DAG | Topological Sort | O(V + E) |
| Dynamic connectivity, "are X and Y connected?" | Union-Find (DSU) | O(alpha(N)) per op |
| Minimum cost to connect all nodes | MST (Kruskal/Prim) | O(E log E) |
| Weighted shortest path with negative edges | Bellman-Ford | O(V * E) |
| Two groups, coloring, odd cycle | Bipartite Check | O(V + E) |
Use V (vertices) and E (edges) bounds to narrow viable algorithms:
| Constraint Range | Viable Techniques | Notes |
|---|---|---|
| V <= 20 | Bitmask DP, brute-force BFS/DFS | Exponential OK |
| V <= 1,000, E <= 10,000 | All techniques, Floyd-Warshall for APSP | O(V^3) still feasible |
| V <= 100,000, E <= 200,000 | BFS, DFS, Dijkstra, Topo Sort, DSU, MST | Standard competitive range |
| V <= 1,000,000 | BFS, DFS, DSU, Kahn's | Avoid O(V log V) heaps if possible |
| Negative weights present | Bellman-Ford, SPFA | Dijkstra invalid |
| Dense graph (E ~ V^2) | Prim (adj matrix), Floyd-Warshall | Adjacency list Dijkstra still works |
| Edges arrive online | Union-Find | Incremental connectivity |
Recognition Signals
Core Idea
BFS explores nodes layer by layer from the source, guaranteeing that the first time a node is reached, it is via the shortest path (in terms of edge count). Use a queue (FIFO) to process nodes in discovery order. Multi-source BFS initializes the queue with all sources at distance 0, treating them as a virtual super-source.
Python Template
from collections import deque
def bfs_shortest_path(graph: dict[int, list[int]], start: int) -> dict[int, int]:
"""Return shortest distance from start to all reachable nodes."""
dist: dict[int, int] = {start: 0}
queue: deque[int] = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in dist:
dist[neighbor] = dist[node] + 1
queue.append(neighbor)
return dist
For multi-source BFS, initialize dist and queue with all sources at distance 0 instead of a single start node.
Key Edge Cases
dist{start: 0}(row, col) as queue elements, check bounds before enqueueCommon Mistakes
.pop(0) is O(N); use deque)Recognition Signals
Core Idea
DFS explores as deep as possible along each branch before backtracking. It naturally discovers connected components, detects cycles, and computes entry/exit times useful for subtree queries. Use iterative DFS with an explicit stack for large graphs to avoid Python's recursion limit.
Python Template (Iterative)
def dfs_iterative(graph: dict[int, list[int]], start: int) -> list[int]:
"""Return all nodes reachable from start in DFS order."""
visited: set[int] = set()
stack: list[int] = [start]
order: list[int] = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
stack.append(neighbor)
return order
Cycle Detection (Directed Graph)
def has_cycle_directed(graph: dict[int, list[int]], n: int) -> bool:
"""Detect cycle in a directed graph with n nodes (0-indexed)."""
WHITE, GRAY, BLACK = 0, 1, 2
color: list[int] = [WHITE] * n
for start in range(n):
if color[start] != WHITE:
continue
stack: list[tuple[int, int]] = [(start, 0)]
color[start] = GRAY
while stack:
node, idx = stack.pop()
if idx < len(graph.get(node, [])):
stack.append((node, idx + 1))
neighbor = graph[node][idx]
if color[neighbor] == GRAY:
return True
if color[neighbor] == WHITE:
color[neighbor] = GRAY
stack.append((neighbor, 0))
else:
color[node] = BLACK
return False
Key Edge Cases
Common Mistakes
sys.setrecursionlimit or iterative)Recognition Signals
Core Idea
Dijkstra greedily expands the closest unvisited node using a min-heap. Each node is finalized when popped from the heap, and relaxation updates neighbor distances. It fails with negative edge weights because a finalized node's distance may later be improvable. The 0-1 BFS variant handles graphs where edges have weight 0 or 1 using a deque instead of a heap.
Python Template
import heapq
def dijkstra(graph: dict[int, list[tuple[int, int]]], start: int) -> dict[int, int]:
"""Return shortest distance from start. graph[u] = [(v, weight), ...]."""
dist: dict[int, int] = {start: 0}
heap: list[tuple[int, int]] = [(0, start)]
while heap:
d, node = heapq.heappop(heap)
if d > dist.get(node, float("inf")):
continue
for neighbor, weight in graph[node]:
new_dist = d + weight
if new_dist < dist.get(neighbor, float("inf")):
dist[neighbor] = new_dist
heapq.heappush(heap, (new_dist, neighbor))
return dist
For 0-1 BFS (weights are only 0 or 1), use a deque: appendleft for weight-0 edges, append for weight-1 edges. This avoids the heap overhead and runs in O(V + E).
Key Edge Cases
distfloat("inf") sentinel, not a magic numberCommon Mistakes
if d > dist guard is essential)visited set and skipping revisits without the distance checkRecognition Signals
Core Idea
Topological sort produces a linear ordering of vertices such that for every directed edge (u, v), u appears before v. It only exists for DAGs. Kahn's algorithm (BFS-based) processes zero-indegree nodes iteratively and naturally detects cycles when the output is shorter than V. DFS-based topo sort appends nodes in reverse finish order.
Kahn's Algorithm (BFS)
from collections import deque
def topological_sort_kahn(graph: dict[int, list[int]], n: int) -> list[int] | None:
"""Return topo order for n nodes (0-indexed), or None if cycle exists."""
indegree: list[int] = [0] * n
for u in graph:
for v in graph[u]:
indegree[v] += 1
queue: deque[int] = deque(v for v in range(n) if indegree[v] == 0)
order: list[int] = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph.get(node, []):
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == n else None
For DFS-based topo sort, use WHITE/GRAY/BLACK coloring. Append nodes to the order when they turn BLACK (all descendants processed), then reverse. A GRAY-to-GRAY back edge indicates a cycle.
Key Edge Cases
Common Mistakes
len(order) == n for cycle detectionRecognition Signals
Core Idea
Union-Find maintains a forest of disjoint sets. Each element has a parent, and the root of the tree is the set representative. Path compression flattens the tree during find, and union by rank keeps the tree balanced. Together they achieve nearly O(1) amortized per operation. To detect a cycle in an undirected graph, check if both endpoints of an edge share the same root before merging.
Python Template
class UnionFind:
def __init__(self, n: int) -> None:
self.parent: list[int] = list(range(n))
self.rank: list[int] = [0] * n
self.components: int = n
def find(self, x: int) -> int:
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, x: int, y: int) -> bool:
"""Merge sets of x and y. Return False if already in same set."""
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False
if self.rank[rx] < self.rank[ry]:
rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1
self.components -= 1
return True
Key Edge Cases
find(x) == x and union(x, x) returns FalseCommon Mistakes
union by rank (degrades to O(N) per find without it)Recognition Signals
Core Idea
A minimum spanning tree connects all vertices with the minimum total edge weight. Kruskal's sorts all edges by weight and greedily adds them using union-find to avoid cycles. Prim's grows the MST from a starting vertex, always adding the cheapest edge to an unvisited neighbor. Kruskal's is simpler for sparse graphs; Prim's with a heap suits dense graphs.
Kruskal's Algorithm
def kruskal(n: int, edges: list[tuple[int, int, int]]) -> list[tuple[int, int, int]]:
"""Return MST edges. edges = [(weight, u, v), ...]. Returns [] if disconnected."""
edges.sort()
uf = UnionFind(n) # uses UnionFind class from above
mst: list[tuple[int, int, int]] = []
for weight, u, v in edges:
if uf.union(u, v):
mst.append((weight, u, v))
if len(mst) == n - 1:
break
return mst if len(mst) == n - 1 else []
Prim's Algorithm
import heapq
def prim(graph: dict[int, list[tuple[int, int]]], n: int) -> int:
"""Return total MST weight. graph[u] = [(v, weight), ...]. -1 if disconnected."""
visited: set[int] = {0}
heap: list[tuple[int, int]] = [(w, v) for v, w in graph.get(0, [])]
heapq.heapify(heap)
total = 0
while heap and len(visited) < n:
weight, node = heapq.heappop(heap)
if node in visited:
continue
visited.add(node)
total += weight
for neighbor, w in graph.get(node, []):
if neighbor not in visited:
heapq.heappush(heap, (w, neighbor))
return total if len(visited) == n else -1
Key Edge Cases
Common Mistakes
if node in visited after heap pop (processes stale entries)Recognition Signals
Core Idea
Bellman-Ford relaxes all edges V-1 times, which is sufficient to propagate shortest distances through any simple path. If a V-th relaxation still improves a distance, a negative cycle exists. Unlike Dijkstra, it handles negative weights correctly. SPFA is a queue-based optimization that only relaxes edges from recently-updated nodes, offering better average-case performance but the same worst case.
Python Template
def bellman_ford(n: int, edges: list[tuple[int, int, int]], start: int) -> list[float] | None:
"""Return distances from start, or None if negative cycle exists.
edges = [(u, v, weight), ...]."""
dist: list[float] = [float("inf")] * n
dist[start] = 0
for _ in range(n - 1):
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# Check for negative cycles
for u, v, w in edges:
if dist[u] + w < dist[v]:
return None
return dist
For K-edges variant (shortest path using at most K edges), run only K iterations instead of V-1, and copy dist before each round to prevent using current-round updates: prev = dist[:], then relax using prev[u].
Key Edge Cases
dist before each round to avoid using current-round updatesCommon Mistakes
Recognition Signals
Core Idea
A graph is bipartite if and only if it contains no odd-length cycle. This is equivalent to being 2-colorable: assign alternating colors via BFS or DFS, and if any edge connects two same-colored nodes, the graph is not bipartite. For disconnected graphs, check each component independently.
Python Template (BFS)
from collections import deque
def is_bipartite(graph: dict[int, list[int]], n: int) -> bool:
"""Check if graph with n nodes (0-indexed) is bipartite."""
color: list[int] = [-1] * n
for start in range(n):
if color[start] != -1:
continue
color[start] = 0
queue: deque[int] = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph.get(node, []):
if color[neighbor] == -1:
color[neighbor] = 1 - color[node]
queue.append(neighbor)
elif color[neighbor] == color[node]:
return False
return True
To extract the two partitions, collect nodes by their color value after a successful check: group_a = [v for v in range(n) if color[v] == 0].
Key Edge Cases
Common Mistakes
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.