Chapter 7: Graphs
Master graph fundamentals from representations to traversals. Learn BFS and DFS on graphs, grid-based graph problems, and classic interview patterns like flood fill, number of islands, and word ladder.
- Topics
- 8
- Demos
- 5
- Videos
- 3
- Min read
- 47
Chapter Overview
Graphs are one of the most versatile data structures in computer science. Unlike trees, which are hierarchical and acyclic, graphs can represent arbitrary relationships between entities --- social networks, road maps, web pages, dependency chains, and much more.
A graph consists of vertices (nodes) and edges (connections between nodes). Edges can be directed or undirected, weighted or unweighted. The way you represent and traverse a graph determines the efficiency of your solution.
This chapter builds your graph intuition from the ground up:
- Graph Fundamentals: Representations, terminology, and when to use each
- BFS & DFS on Graphs: Adapting tree traversals to handle cycles and disconnected components
- Shortest Path: Finding minimum-hop paths in unweighted graphs
- Grid as Graph: Treating 2D matrices as implicit graphs
- Classic Problems: Flood fill, number of islands, and word ladder
Chapter Roadmap
Click any topic to jump in
Graph Fundamentals
Vertices, edges, adjacency lists, and matrices — choosing the right representation for your data.
Queue or stack
BFS on Graphs
Queue-based traversal with a visited set — handling cycles and disconnected components.
DFS on Graphs
Recursive or stack-based deep exploration — finding connected components and cycles.
Shortest Path
BFS gives optimal distances in unweighted graphs — plus parent tracking for full path reconstruction.
Grids and adjacency
Matrix as Graph
Grids as implicit graphs — each cell is a node, neighbors are the 4 or 8 adjacent cells.
Flood Fill
Classic connectivity problem — BFS or DFS from a seed cell to fill a region.
Islands and word ladders
Number of Islands
Count connected components in a grid — iterate, trigger BFS/DFS, mark visited.
Word Ladder
Words as nodes, one-letter changes as edges — BFS finds the shortest transformation sequence.
Topics
8 topics in this chapter
A delivery app needs routes between neighbourhoods, a social network records who knows whom, and a package manager records which library depends on which. None of these fits a tree. Roads loop back on themselves, friendships run both ways, and one library can be required by many others, so a node can have several parents or none.
The previous chapter, Breadth First Search, traversed trees, where every node has exactly one parent and no path returns to where it started. A graph drops both restrictions. It is just a set of vertices and a set of edges between them, and it may contain cycles, isolated vertices, and several separate pieces. Edges can be directed or undirected, weighted or unweighted, and each combination models a different kind of problem.
Before running any algorithm on a graph you have to store it, and the storage choice fixes the cost of every later step. We first set up the vocabulary of vertices, edges, and degrees. Then we compare the two standard representations, the adjacency list and the adjacency matrix, and see when each one wins.
Definition
A graph is a set of vertices and a set of edges , each edge joining a pair of vertices. Undirected edges are unordered pairs, directed edges ordered pairs. An adjacency list stores each vertex's neighbours in space; an adjacency matrix stores a table of edge flags in space.
In this topic
- 1Vertices and Edges
- 2Adjacency List Representation
- 3Adjacency Matrix Representation
Vertices and Edges
Before choosing an algorithm you must name the problem's objects and relationships. A vertex is an entity, such as a city, a user, or a course, and an edge joins two vertices. In an undirected graph an edge works both ways, like a two-way road; in a directed graph an edge goes from to only, like following someone online. The degree counts the edges touching , and directed graphs split it into in-degree and out-degree. Each undirected edge touches two endpoints, so degrees always sum to , the handshaking lemma. Modelling a one-way relationship as undirected invents paths that do not exist.
A graph has vertices and edges. In an undirected graph, each edge contributes to both and , giving the handshaking lemma . In a directed graph, . The maximum edge count is for a simple undirected graph (complete graph ) or for a directed one. Sparse graphs have ; dense graphs have .
Given 4 cities connected as: A-B, B-C, C-D, A-D. How many vertices, edges, and what is the degree of each vertex?
Adjacency List Representation
Graph algorithms ask one question over and over: what are the neighbours of ? An adjacency list answers it directly by storing, for each vertex, the list of vertices it connects to, usually a dict of lists or a list of lists indexed by vertex number. It uses space, where is the number of vertices and the number of edges; an undirected edge is stored twice, once in each endpoint's list. Listing the neighbours of costs , which is optimal, but testing whether one specific edge exists means scanning a list. Most interview graphs are sparse, with far below , so this is the default.
An adjacency list stores vertex entries, each mapping to a list of neighbors. Total space is : each vertex contributes overhead, and each edge appears in 1 or 2 lists (directed or undirected). Checking whether edge exists requires scanning 's neighbor list — worst case. Iterating all neighbors of is , which is optimal. For sparse graphs with , total space is , making adjacency lists dramatically more efficient than matrices.
Represent this undirected graph as an adjacency list: edges are (0,1), (0,2), (1,3), (2,3).
Adjacency Matrix Representation
When a problem repeatedly asks whether two particular vertices are connected, scanning lists is slow. An adjacency matrix is a array with when an edge runs from to and 0 otherwise; weighted graphs store the weight instead. An edge test is a single lookup, and an undirected graph gives a symmetric matrix. The costs are space however few edges exist, and time to list one vertex's neighbours, because the whole row must be read. For 100,000 vertices that is 10 billion cells even when most are zero, so matrices suit small or dense graphs.
An adjacency matrix has if edge exists. Space is always regardless of . Edge existence queries are , but iterating the neighbors of vertex is — you must scan the entire row. The matrix is symmetric () for undirected graphs. Matrix powers have elegant meaning: counts walks of length from to . For dense graphs with , matrices and lists use the same space, but matrices give edge lookups.
Build an adjacency matrix for 4 vertices with edges: (0,1), (0,2), (1,3), (2,3).
Related Problems on PixelBank
A contact-tracing team needs everyone within two contacts of an infected person, and a network engineer needs every router reachable from a given one. The previous topic, Graph Fundamentals, showed how to store such networks as adjacency lists. The queue-based BFS from the previous chapter nearly solves both problems, but it was written for trees, where each node is reached along exactly one path.
In a graph, the same vertex can be reached along many paths, and a cycle can lead straight back to the start. Tree BFS run on a triangle 0-1-2 would enqueue 0 again as a neighbour of 1, and the queue would never empty. The fix is a visited set: mark a vertex the moment it is enqueued, and never enqueue a marked vertex again. With that single addition, BFS processes each vertex once and each edge a constant number of times, O(V + E) overall.
We first add the visited set and see why marking on enqueue beats marking on dequeue. Then we recover the level structure to measure hop distances, and finally restart BFS to cover graphs that are split into several disconnected pieces.
Definition
Breadth-first search on a graph explores vertices in order of hop distance from a source, using a FIFO queue plus a visited set. A vertex is marked visited when it is first enqueued and is never enqueued again, so each vertex is processed once and each edge is examined at most twice, giving time and extra space.
In this topic
- 1BFS with Visited Set
- 2Level-Order Traversal on Graphs
- 3Handling Disconnected Components
BFS with Visited Set
Tree BFS relies on every node having one parent, so each node is enqueued once. In a graph a vertex may be adjacent to many already-seen vertices, and without bookkeeping it is enqueued again from each of them, or forever around a cycle. The visited set fixes this: when is dequeued, each neighbour not yet in the set is added to it and enqueued. Marking on enqueue rather than on dequeue matters, because marking late lets wait in the queue several times at once. Each vertex then enters the queue once and each undirected edge is examined twice, so BFS costs time and space.
Graph BFS extends tree BFS by maintaining a visited set to prevent revisiting nodes. Without it, cycles would cause infinite loops. When dequeuing , we enqueue each unvisited neighbor and mark it visited. Each node is enqueued at most once, and each edge is examined twice (once from each endpoint) in an undirected graph, giving time. Space is for the visited set plus the queue. The 'mark on enqueue' (not on dequeue) pattern prevents duplicate enqueues, which would otherwise multiply work by per node.
Run BFS starting from node 0 on graph: 0-1, 0-2, 1-3, 2-3, 3-4. What order are nodes visited?
Level-Order Traversal on Graphs
Plain BFS returns vertices in distance order but does not say where one distance ends and the next begins. The previous chapter solved this for trees with the level-size snapshot, and it carries over unchanged: at the start of each round the queue holds exactly the vertices at distance from the source, so processing exactly len(queue) of them completes level , while the neighbours they discover form level . Alternatively, set dist[v] = dist[u] + 1 when is first enqueued. Either way every vertex gets its hop distance in time. The visited set is still required, or a vertex reached from two parents would appear in two levels.
Level-order BFS on graphs uses the same queue-size snapshot trick as trees: at the start of each iteration equals the number of vertices at the current BFS depth. The BFS tree rooted at the source is a spanning tree where edge is either a tree edge (parent-child) or a cross/back edge — but cross edges in undirected BFS connect only same-level or adjacent-level vertices (no back edges to earlier levels). Total time remains .
In the graph 0-1, 0-2, 1-3, 2-4, 3-5, 4-5, what are the BFS levels starting from 0?
Handling Disconnected Components
A BFS from one source reaches only the vertices joined to it by some path, its connected component. Real graphs are often split: users with no mutual friends, or road networks on separate islands. To visit every vertex, loop over all vertices and start a fresh BFS from each one that is still unvisited, sharing one visited set across all the runs. Each start discovers exactly one new component, so counting starts counts components. The total cost stays , because the shared set stops any vertex or edge from being processed twice. Forgetting the outer loop is a common bug that passes connected tests and fails silently otherwise.
In a disconnected graph, a single BFS from one source only reaches its connected component. To visit every vertex, iterate through all vertices and start a new BFS from any unvisited one. Total time is still because the outer loop is and all BFS calls together examine each edge at most twice. The number of BFS restarts equals the number of connected components . This pattern generalizes to counting components, labeling them, or running any component-local algorithm.
Graph has edges: 0-1, 1-2, 3-4. How many BFS calls are needed to visit all nodes?
Related Problems on PixelBank
A maze solver wants to know whether any exit can be reached, a build tool wants to know whether a chain of imports loops back on itself, and a network monitor wants to know which machines form isolated clusters. None of these needs shortest distances. They need to explore everything reachable, and often they need the path currently being explored.
The previous topic, BFS on Graphs, spread outward from the source one level at a time. Depth-first search instead follows one path as far as it can go and backtracks only when every neighbour of the current vertex has been visited. Depth First Search & Trees, in Chapter 4, already worked this way, and the graph version needs the same visited set as graph BFS, since a cycle would otherwise send it around forever. Because DFS keeps just the current path on its stack, questions about paths, cycles, and ordering become natural to answer.
We first write recursive DFS and trace it. Then we replace the call stack with an explicit one so the search survives very deep graphs, and finally we use DFS to label every connected component.
Definition
Depth-first search explores a graph by moving from the current vertex to an unvisited neighbour for as long as one exists, and backtracking to the previous vertex when none does. It uses a stack, either the recursion call stack or an explicit list, plus a visited set, and runs in time with extra space.
In this topic
- 1Recursive DFS with Visited Set
- 2Iterative DFS with Explicit Stack
- 3Connected Components via DFS
Recursive DFS with Visited Set
The recursive form mirrors the definition: mark the current vertex visited, then call DFS on each neighbour that is not yet visited. When the loop over neighbours ends, the function returns, and that return is the backtracking step, handled automatically by the call stack. Each vertex is marked once and each adjacency list is scanned once, giving time. The recursion depth equals the length of the current path, up to on a long chain, and Python's default limit of 1,000 frames raises RecursionError on a path graph of a few thousand vertices. The visit order depends on the order of each neighbour list.
Recursive DFS explores as deep as possible before backtracking, using the call stack implicitly. Each call examines one vertex, marks it visited, and recurses on each unvisited neighbor. Time is — identical to BFS — because each vertex is visited once and each edge is examined once per endpoint. Space is for the visited set plus for the call stack, where is the DFS tree depth (up to in the worst case of a path graph). Python's default recursion limit (1000) can be hit on long paths; use sys.setrecursionlimit or go iterative.
Run DFS from node 0 on graph: 0-1, 0-2, 1-3, 2-4, 3-4. What is one possible order?
Iterative DFS with Explicit Stack
Deep graphs overflow the call stack, so production code often replaces recursion with an explicit list used as a stack. Push the start vertex, then repeatedly pop a vertex, skip it if it is already visited, and otherwise mark it and push its unvisited neighbours. A stack is last in, first out, so the neighbour pushed last is explored first, and pushing each neighbour list in reverse reproduces the recursive visit order. Marking happens on pop, not on push: a vertex may be pushed by several neighbours before it is reached, and marking on push still visits everything, but no longer in depth-first order. Time stays .
Iterative DFS replaces the call stack with an explicit list stack, popping the top and pushing unvisited neighbors. Note the subtle difference from recursion: when you push multiple neighbors, the order of exploration is the reverse of the push order (LIFO). To match recursive left-to-right order, push neighbors in reverse. Time and asymptotic space are unchanged from recursive DFS, but iterative avoids Python's recursion limit and has slightly lower constant factors due to no function call overhead.
Trace iterative DFS from node A on graph: A-B, A-C, B-D, C-D.
Connected Components via DFS
The outer-loop pattern from BFS works identically with DFS: scan the vertices, and whenever one is unlabelled, launch a DFS from it that writes the current component number on every vertex it reaches. Each launch paints one entire component before returning, so the number of launches equals the number of components, and the label array afterwards answers whether and are connected in . The total cost is , since the shared labels stop any vertex from being painted twice. With vertices and edges there are at least components, because each edge merges at most two.
To count connected components, iterate over all vertices; whenever we hit an unvisited one, start a DFS that marks its entire component and increment the counter. The total work across all DFS calls is because every vertex and edge is processed exactly once. The number of components equals the number of DFS trees in the DFS forest. For vertices and edges in an undirected graph, a lower bound on components is (each edge can merge at most two components).
Find connected components in: 0-1, 1-2, 3-4, 5.
Related Problems on PixelBank
A subway app asks for the route between two stations with the fewest stops, and a social network shows the degrees of separation between two people. Both want a shortest path in a graph where every edge costs the same: one stop, or one introduction.
The previous topic, DFS on Graphs, can find a path but not the shortest one. It commits to the first branch it tries, so even when the target is one edge from the source, DFS may wander through most of the graph and report a long detour. BFS, from earlier in this chapter, visits vertices in order of hop distance, so the first time it reaches a vertex, it has reached it along a path with the fewest possible edges. That property makes BFS the shortest-path algorithm for unweighted graphs, in O(V + E) time. Weighted edges, where some stops take longer than others, need Dijkstra's algorithm from the next chapter.
We first show why first arrival is shortest and stop the search as soon as the target appears. Then we record parent pointers during the search so we can rebuild the actual route, not only its length.
Definition
In an unweighted graph, the shortest-path distance is the minimum number of edges on any path from to , or infinity if no path exists. BFS from computes it for every vertex in time by setting dist[v] = dist[u] + 1 when is first discovered from , and a parent map that stores lets the path itself be rebuilt.
In this topic
- 1BFS as Shortest Path Algorithm
- 2Path Reconstruction with Parent Tracking
BFS as Shortest Path Algorithm
Why is the first arrival shortest? The BFS queue always holds vertices from at most two consecutive distances, and , with every distance- vertex ahead of every distance- vertex. Vertices therefore leave the queue in non-decreasing order of distance, and when is first discovered from , no vertex closer to the source is still waiting to discover it. Setting dist[v] = dist[u] + 1 at that moment is final, so the search can stop as soon as the target is discovered. The argument needs every edge to cost 1; with unequal weights, a path with more edges can be cheaper.
BFS computes the shortest-path distance in an unweighted graph — that is, the minimum number of edges on any path. The proof is by induction on distance: suppose BFS correctly labels all vertices at distance . A vertex at distance must have at least one neighbor at distance , and when BFS dequeues , it will enqueue (if unvisited), assigning . Total time is , which is optimal since any algorithm must at least read the graph.
Find the shortest path from A to E in graph: A-B, A-C, B-D, C-D, D-E.
Path Reconstruction with Parent Tracking
Distances alone do not give directions. To return the route, store parent[v] = u at the moment BFS first discovers from , the same moment dist[v] is set, which costs only extra space. The parent pointers form a BFS tree rooted at the source, and every path in that tree is a shortest path. To rebuild the route to a target , follow parent pointers from back to the source, collecting vertices, then reverse the list. A target that never received a parent, and is not the source, is unreachable. Overwriting a parent after the first discovery breaks this guarantee and can produce a longer path.
To reconstruct the path (not just its length), maintain a parent map where parent[v] = u if was the vertex that first enqueued . Once BFS finishes, walk backward from the target: , then reverse. Space is for the parent map. This is the standard pattern for any shortest-path algorithm (Dijkstra, Bellman-Ford, A*): record predecessors during relaxation, reconstruct at the end.
Given parent map: {B: A, C: A, D: B, E: D}, reconstruct path from A to E.
Related Problems on PixelBank
A robot vacuum plans a route across a floor plan stored as a grid of open and blocked squares, and a game counts how many moves a character needs to reach a treasure on a tile map. Neither problem arrives as an adjacency list. The input is a 2D array.
The previous topic, Shortest Path (Unweighted), ran BFS over explicit vertices and edges. A grid is a graph too, only an implicit one: each cell (r, c) is a vertex, and edges join it to the cells directly above, below, left, and right, when those cells exist and are passable. Building an adjacency list for a million-pixel image would waste memory, so grid code computes neighbours on the fly from direction vectors and checks the grid boundaries every time. Everything from earlier in this chapter, the visited set, BFS levels, and DFS components, then applies unchanged.
We first describe the implicit graph and its direction vectors. Then we look at the boundary checks that stop index errors and silent wraparound, and finally run BFS and DFS on grids, including ways to mark visited cells without extra memory.
Definition
A grid graph treats each cell of an matrix as a vertex, with edges to its 4 neighbours and , or 8 with diagonals, that lie inside the grid and are passable. Neighbours are generated from direction vectors rather than stored, so BFS and DFS run in time without building an adjacency list.
In this topic
- 1Grid as Implicit Graph
- 2Boundary Checking
- 3BFS and DFS on Grids
Grid as Implicit Graph
Storing a grid's adjacency list would repeat what the coordinates already say, because the neighbours of follow from arithmetic. Grid code instead keeps a short list of direction vectors, [(0, 1), (0, -1), (1, 0), (-1, 0)] for 4-connectivity, and computes each neighbour as . An grid has vertices and fewer than edges, since each cell has at most 4 neighbours and every edge is shared by two cells. Adding the four diagonal vectors gives 8-connectivity, which changes which cells count as touching, and so changes the answer to any region-counting question.
An grid is a graph with vertices where each cell connects to its 4-neighborhood (or 8-neighborhood including diagonals). The edge count is : each interior cell has 4 edges, corners have 2, edges have 3, giving total edges in 4-connectivity. BFS/DFS on this implicit graph runs in time without ever materializing an adjacency list — we compute neighbors on the fly via coordinate arithmetic.
In a 3x3 grid, what are the valid 4-directional neighbors of cell (1, 1)?
Boundary Checking
A neighbour computed by arithmetic may not exist. For a cell on the top row, , and in Python grid[-1][c] raises no error: it silently reads the bottom row, so the search wraps around the edge and joins cells that are not adjacent. Every neighbour must pass 0 <= nr < rows and 0 <= nc < cols before it is read, with the check written before the index so short-circuit evaluation protects it. Most problems then add a second filter on the cell's value, such as open versus blocked. Both checks cost per neighbour and never change the complexity.
Before accessing neighbor , we must verify and . Without this check, out-of-bounds indices raise IndexError or wrap around negatively in Python lists. The check is , so it does not affect asymptotic time. A common optimization is padding: wrap the grid with a sentinel border of size 1, reducing per-step checks at the cost of extra space. For million-cell grids, the savings are negligible — explicit bounds checks are usually cleanest.
In a 3x4 grid, which directions are valid from cell (0, 3)?
BFS and DFS on Grids
With neighbours generated and bounds checked, grid search is ordinary graph search. BFS from a start cell gives the fewest moves to every reachable cell, since each move is one unweighted edge, while DFS is the simpler way to explore a whole connected region. Visited cells can be tracked in a set of (r, c) pairs, in a boolean matrix, or by overwriting the grid itself, which saves memory but destroys the input. Both searches take time on an grid. Recursive DFS is risky on large grids: a snake-shaped region of a million cells means a recursion a million frames deep.
BFS on a grid finds shortest paths in unweighted 4- or 8-connectivity, useful for games, robotics, and pathfinding. DFS explores one connected region at a time, good for flood fill, island counting, and maze generation. Time is for both, since each cell is visited once. Space differs: BFS queues can grow to for a diagonal front; DFS recursion depth can reach on spiral patterns, so iterative DFS (or BFS) is safer for large grids to avoid stack overflow.
Find the shortest path from (0,0) to (2,2) in:
0 0 0
1 1 0
0 0 0
where 0 = open, 1 = blocked.
Related Problems on PixelBank
In an image editor, the paint bucket tool recolours the region you click: every pixel of the same colour connected to the clicked one changes, while pixels of that colour elsewhere in the image stay as they are. Minesweeper uses the same operation when a click on an empty square opens every connected empty square.
The previous topic, Matrix as Graph, treated a grid as an implicit graph searched with BFS or DFS. Flood fill is that search with one filter and one side effect: move only into neighbours whose colour equals the original colour of the start cell, and recolour each cell as it is reached. The recolouring doubles as the visited mark, so no separate set is needed, and only the region's own cells are processed, O(k) work for a region of k cells rather than a scan of the whole image.
That shortcut hides one trap. If the new colour equals the original colour, recolouring marks nothing, and the search never stops. We first implement flood fill recursively and with a queue, then isolate that same-colour edge case and the one-line guard that prevents it.
Definition
Flood fill takes a grid, a start cell, and a new colour, and gives the new colour to the start cell and every cell connected to it through 4-neighbours of the same original colour. It is a BFS or DFS restricted to cells of the original colour, runs in time for a region of cells, and must return at once when the new colour equals the original.
In this topic
- 1The Flood Fill Algorithm
- 2Early Termination Check
The Flood Fill Algorithm
Flood fill first reads the original colour of the start cell, because that cell is about to be overwritten. It then recolours the start cell and, recursively or with a queue, visits each 4-neighbour that is in bounds and still has the original colour, recolouring it and continuing from there. A neighbour that has already been recoloured no longer matches, so the new colour acts as the visited set. Only cells in the region and their immediate boundary are examined, so the cost is for a region of cells, reaching when the whole grid is one colour. Recursive versions can also reach depth .
Flood fill starts at a seed cell with original color , recursively replacing all connected same-colored cells with the new color . Using BFS: enqueue the seed, mark it , then for each dequeued cell, enqueue any 4-neighbor still equal to . Marking happens on enqueue, not dequeue, to prevent duplicates. Time is where is the size of the connected region, since each cell in the region is processed once. Space is worst case for the queue.
Flood fill starting at (1,1) with new color 2:
1 1 1
1 1 0
1 0 1
Early Termination Check
Using the new colour as the visited mark has one failure case. If the start cell already has the new colour, recolouring changes nothing, so every neighbour of that colour still matches the original, and the fill steps back into cells it has just processed. Recursive DFS bounces between two adjacent cells until Python raises RecursionError, and a BFS re-enqueues them forever. The fix is one line at the top: if the original colour equals the new colour, return the grid unchanged, which is also the correct answer. A separate visited set would prevent the loop as well, at the cost of extra memory for a region of cells.
If the seed cell already has the target color (), naive flood fill loops forever because the 'color change' never distinguishes visited from unvisited cells. The standard fix is an early return: if original == new: return grid. This handles the edge case in . Alternatively, maintain an explicit visited set — slightly more memory but safer for problems where 'no-op' fills should still compute region statistics.
What happens if we flood fill (0,0) with color 1 on grid [[1, 1], [1, 0]] without the early termination check?
Related Problems on PixelBank
Given a satellite map where each cell is land or water, how many separate islands are there? Microscopy software asks the same question about cells on a slide, and circuit inspection asks it about separate copper traces on a board.
The previous topic, Flood Fill, recoloured the single region containing a given start cell. Counting islands needs every region, and no start cell is given. The answer combines flood fill with the outer loop from BFS on Graphs. Scan the grid row by row, and whenever an unvisited land cell appears, it must belong to an island not seen before, so increment the count and flood the whole island to mark it visited. The scan then skips that island's remaining cells, so each island is counted exactly once, in O(mn) total time. In graph terms, the islands are the connected components of the grid graph restricted to land cells, which is the same component counting done earlier in this chapter.
We first build the count-and-sink algorithm and trace it on a small map. Then we compare marking visited cells in the grid itself with keeping a separate visited set, and when each is appropriate.
Definition
Number of Islands counts the connected components of land cells in a grid of '1' (land) and '0' (water), where two land cells are connected when they share a side, not merely a corner. A row-major scan launches a BFS or DFS from each unvisited land cell and increments the count, and that search marks the whole island. Total time is .
In this topic
- 1Islands as Connected Components
- 2In-Place Marking vs Visited Set
Islands as Connected Components
Counting islands is counting connected components, where the vertices are land cells and edges join land cells that share a side. The scan visits cells in row-major order. At an unvisited land cell it has found a new island, since any earlier cell of the same island would already have marked it; it increments the count and runs DFS or BFS to sink the whole island. Each cell is read once by the scan and at most once more by a search, so the cost is for rows and columns. Diagonal cells do not connect unless the problem says they do, a frequent source of wrong counts.
An island is a maximal connected region of land cells ('1') in a grid, where connectivity is 4-directional. Counting islands is equivalent to counting connected components in the implicit graph. The algorithm: iterate all cells; whenever we find an unvisited land cell, increment the counter and run BFS/DFS to mark the entire island as visited. Total time is because each cell is visited at most once during the outer loop and at most once during a component's traversal. Space is worst case for the visited set or recursion.
Count islands in:
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
In-Place Marking vs Visited Set
The search must know which land cells it has already counted. In-place marking overwrites each visited '1' with '0', sinking it, so the grid itself is the visited record and no memory is used beyond the search's stack or queue. The price is that the caller's grid is destroyed, which breaks any code that reuses it and is unsafe when other threads read it. A separate visited set of (r, c) pairs, or a boolean matrix, keeps the input intact at extra space, and in Python the boolean matrix is far smaller than a set of tuples. In an interview, ask whether mutating the input is allowed.
Two options for tracking visited cells: (1) maintain a separate visited set of coordinates, extra space; (2) mutate the grid in-place, e.g., change '1' to '#' or '0'. In-place marking uses extra space (besides the recursion/queue) but destroys the input — problematic if the grid is needed later or if the function contract forbids mutation. For interviews, always ask 'can I modify the input?' before choosing.
After processing the first island in [[1,1,0],[1,0,0],[0,0,1]], what does the grid look like with in-place marking?
Related Problems on PixelBank
Lewis Carroll's puzzle Doublets, published in 1879, asks you to turn one word into another by changing one letter at a time, where every step must itself be a real word: cold, cord, card, ward, warm. A puzzle generator that rates difficulty and a spell checker that suggests nearby words both need the shortest such chain.
The previous topic, Number of Islands, searched a graph hidden in a grid. Here the graph is hidden in a dictionary: each word is a vertex, and two words share an edge when they differ in exactly one position. The shortest chain is then a shortest path, which BFS finds, as in Shortest Path (Unweighted). The real difficulty is the edges. Comparing every pair of words costs O(N^2 L) for N words of length L, far too slow for a large dictionary. Generating each word's one-letter variants, or grouping words by wildcard patterns such as h*t, finds the neighbours much faster.
We first model the words as graph nodes and generate neighbours efficiently. Then we run BFS level by level to count the shortest transformation sequence, and see how it reports when no ladder exists.
Definition
Word Ladder asks for the number of words in the shortest sequence from a begin word to an end word, where consecutive words differ in exactly one letter and every word after the first is in a given dictionary. Treating words as vertices and one-letter differences as edges makes it an unweighted shortest-path problem solved by BFS, with the answer counting words rather than edges.
In this topic
- 1Words as Graph Nodes
- 2BFS for Shortest Transformation
Words as Graph Nodes
The graph is never given; it must be built from the dictionary. Comparing every pair of words for exactly one mismatch costs for words of length : about 6 billion character checks for 50,000 five-letter words. Two faster schemes exist. Wildcard buckets replace each position with * in turn, so hot yields ot, ht, and ho*, and any two words that share a bucket are neighbours; building every bucket costs . Letter substitution tries all 26 letters at each position and keeps candidates found in a hash set of the dictionary, which costs per word.
Model the word ladder problem as a graph where each word is a vertex and two words are connected iff they differ in exactly one letter. Building this graph naively costs time where is the word count and is word length — compare every pair. A faster approach uses 'generic' patterns: for each word, create wildcards like h*t, *ot, ho* and group words sharing a pattern — for neighbor generation on the fly during BFS.
Find neighbors of "hot" in dictionary ["hot", "dot", "dog", "lot", "log", "cog"].
BFS for Shortest Transformation
Once neighbours can be generated, the shortest transformation is a BFS from the begin word. Level holds every word reachable in exactly changes, so the level at which the end word first appears is the answer, counted in words rather than edges. Remove each word from the dictionary set, or mark it visited, when it is first enqueued; otherwise words that share buckets keep re-enqueueing each other. With letter substitution each dequeued word costs , including building candidate strings, so BFS runs in . Bidirectional BFS, growing from both ends until the frontiers meet, often explores far fewer words.
Given the unweighted graph of words, the shortest transformation sequence from beginWord to endWord is the BFS distance plus one (vertices, not edges). BFS guarantees the minimum number of steps because it explores in order of distance. Time is using pattern-based neighbor generation: positions times to create each pattern, across all words. Bidirectional BFS can cut this to by meeting in the middle.
Trace BFS from "hit" to "cog" with dictionary ["hot","dot","dog","lot","log","cog"].