A graph is a set of nodes and the edges between them. Almost every graph problem starts the same way: pick a representation, then walk it with DFS or BFS while a visited set stops you from going around in circles. Counting connected components (how many separate pieces the graph falls into) is the first thing you build on top of that walk.
Storing the graph#
You have two standard choices, and the decision almost always comes down to density: how many edges you have compared with the number of nodes.
| Adjacency list | Adjacency matrix | |
|---|---|---|
| Shape | adj[u] = list of neighbors |
grid[u][v] = 1 (or the weight) |
| Memory | ||
Is u–v an edge? |
scan | |
| Best for | Sparse graphs (social networks, maps) | Dense graphs, or input already given as a matrix |
Default to the adjacency list. Real data is sparse: you might have 500 friends on a social network with billions of users, and a matrix would store a row of billions of zeros for every user. Use a matrix only when most nodes are connected (so the space is justified) or the input already is a matrix.
For an undirected edge, add it in both directions (adj[u].append(v) and adj[v].append(u)). Forgetting the second line silently turns the graph into a directed one.
The idea#
Once the graph is stored, you walk it. DFS and BFS both visit every reachable node once. They differ only in the order, which comes from the container they use.
- DFS ("the maze runner") goes as deep as it can, then backtracks. With recursion, the call stack remembers where you came from, so backtracking needs no extra code.
- BFS ("a stone dropped in a pond") visits all neighbors at distance 1, then distance 2, and so on, using a FIFO queue. Because it expands in layers, the first time it reaches a node is along a path with the fewest edges.
Both need a visited set. Without it, if 0 connects to 1 and 1 connects to 0, the walk bounces between them forever. Each node is processed once and each edge is looked at a constant number of times, so both run in .
Template#
Build an undirected adjacency list, then count components by starting a new walk from every node that no earlier walk reached. Either walk works inside the loop.
for each edge (u, v):
add v to adj[u]; add u to adj[v] # undirected: both ways
dfs(node):
add node to visited
for each nei in adj[node] not in visited: dfs(nei)
bfs(start):
add start to visited; push start onto queue
while queue is not empty:
node = pop from the front of queue
for each nei in adj[node] not in visited:
add nei to visited # mark on enqueue
push nei onto queue
count = 0
for each node not in visited:
walk from node; count += 1 # new walk, new component
return count
In BFS, mark a node visited the moment you enqueue it. If you wait until you pop it, two different parents can both add it to the queue, and the duplicate work grows with the number of edges.
How to spot it#
Problems rarely say "use BFS". The wording gives it away.
| The question asks... | The graph is... | Use |
|---|---|---|
| Shortest path | Unweighted | BFS |
| Weighted, non-negative | Dijkstra | |
| Has negative weights | Bellman-Ford (rare) | |
| Connectivity | Edges keep arriving (merging) | Union-Find |
| Fixed | DFS / BFS | |
| Ordering | Dependencies | Topological sort |
| Connect everything cheaply | Weighted | MST (Kruskal) |
| Cycles | Undirected | Union-Find (or DFS with parent) |
| Directed | Topological sort (or DFS) |
When the graph is fixed and you only need to walk it, the wording also tells BFS and DFS apart:
- BFS: "shortest path", "fewest steps", "minimum number of moves", "nearest", "level order". It guarantees the fewest hops in an unweighted graph.
- DFS: "does a path exist?", "can we reach...?", "count all paths", "cycle detection", "topological sort", puzzle solving. It suits exhaustive searches that go deep and undo choices. It is not guaranteed to find the shortest path.
Memory: wide graphs vs deep graphs#
The two traversals remember different things, which matters on huge graphs. BFS holds the whole frontier (the current layer). DFS holds only the current path from the start.
Picture a star: one center node joined to 1,000,000 leaves. BFS pops the center and immediately queues all million leaves. DFS walks center → leaf → back, so its stack never holds more than two nodes.
- Wide graph: BFS uses a lot of memory, so DFS is better.
- Deep graph (a long chain): DFS's recursion stack grows with the depth, so BFS is safer. In Python this is also where you hit the recursion limit.
Connected components and input format#
How the input arrives can decide the complexity before you write any traversal.
- An matrix: you don't need to build a list. Walk straight on the matrix by scanning row
nodefor 1s. Every visit scans columns, so the total is , which is unavoidable because reading the input already costs that. - An edge list: building the list is , and a walk visits each node once and each edge twice, so the total is . You avoid the scan.
Component counting also raises a question about the visited set: should every walk share it? It can be global (shared by every walk, as in component counting) or fresh for each search. A fresh one is needed when the relation is directed and you want the reach of each start on its own.
Pitfalls#
These are the mistakes that most often cost a correct answer.
- Adding an undirected edge in only one direction.
- Marking visited on pop in BFS, which queues the same node many times.
- Using DFS when the question asks for the fewest steps.
- Recursion depth: a 10,000-node chain overflows Python's default recursion limit. Switch to an explicit stack or BFS.
- Calling a relation symmetric when it isn't: check whether "a reaches b" implies "b reaches a" before adding both directions.
Problems in this pattern#
These problems use this pattern. Solve them before you open the hints below. Start with the first two.
- 997. Find the Town Judge
- 547. Number of Provinces
- 323. Number of Connected Components in an Undirected Graph
- 2101. Detonate the Maximum Bombs
- 2685. Count the Number of Complete Components
- 133. Clone Graph
- 797. All Paths From Source to Target
- 332. Reconstruct Itinerary
Hints#
Open a hint only after you have tried the problem for a while.
Hint: Number of Provinces
The input is already an matrix. Walk on it directly by scanning row node for 1s, with no list to build. The total is , the same as reading the input.
Hint: Number of Connected Components in an Undirected Graph
The input is an edge list, so build an adjacency list in and count walks. The total is .
Hint: Detonate the Maximum Bombs
Bomb i reaches bomb j when the squared distance is at most ; compare squares, no sqrt. The relation is directed: a big bomb can reach a small one that can't reach back. Run a separate walk with a fresh visited from every bomb, and take the largest count.
Hint: Count the Number of Complete Components
A complete component with nodes has edges. Sum the degrees inside the component during the walk. Every edge counts twice, so the check is N * (N - 1) == degree_sum.
Hint: Clone Graph
Keep a map from each original node to its copy. One clean way is two passes: create a copy of every node, then wire each copy's neighbors through the map. Return the copy of the node you were given (copies[node]), never a fixed key such as "node 1". Space is , because the copies carry every edge too.
Hint: Find the Town Judge
No traversal is needed, only in-degrees and out-degrees. The judge is trusted by everyone else and trusts no one, so one pass over the edges decides it.
Hint: All Paths From Source to Target
You need every path in a DAG (a directed graph with no cycles), so there is no visited set and no bound. graph[node] is already an adjacency list. A DAG can have up to source-to-target paths, and copying each costs , so time and space are .
Hint: Reconstruct Itinerary
You need a path that uses every ticket exactly once (an Eulerian path). Sort each airport's destinations, DFS by always taking the smallest unused one, and add an airport to the route only after all its edges are used. Reverse the route at the end.
Key takeaways#
- Default to an adjacency list; use a matrix for dense graphs or when the input already is one.
- DFS remembers the path, BFS remembers the frontier: wide graphs favor DFS, deep graphs favor BFS.
- In BFS, mark visited when you enqueue.
- Components = the number of times you have to start a new walk.
- The input format can fix the complexity: an matrix forces .