Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Word Ladder
Transform one word into another by changing a single letter at a time, where every step must be a real word. The shortest such chain is a shortest path in a hidden graph — so it is a job for breadth-first search. Return the length of the shortest ladder. Solve it in Python or TypeScript, with hidden tests.
The problem
Given a begin word, an end word, and a list of allowed words, return the number of words in the shortest transformation sequence from begin to end, changing exactly one letter at a time, where every intermediate word (and end) must be in words. Count both endpoints. Return 0 if no such sequence exists.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement ladder_length(begin, end, words): BFS from begin, where a word’s neighbors are the words in the set reachable by changing one letter. Track the depth (word count); the first time you reach end, return it. Return 0 if BFS exhausts.
Examples and constraints 5 examples
begin = 'hit', end = 'cog', words = ['hot','dot','dog','lot','log','cog']5begin = 'hit', end = 'cog', words = ['hot','dot','dog','lot','log']0begin = 'cat', end = 'dog', words = ['cot','cog','dog']4begin = 'hit', end = 'cog', words = ['hot','cog']0begin = 'a', end = 'c', words = ['b','c']2- All words have the same length and are lowercase;
beginneed not be inwords, butendmust be. - Each step changes exactly one letter and must land on a word in the list.
- The answer counts words (nodes), not steps — the shortest chain of length k has k words. BFS gives the shortest.
Hints 4 ideas
- Put the word list in a set for O(1) membership. If
endis not in it, the answer is 0 immediately. - BFS level by level from
begin, carrying the number of words used so far (start at 1). - Neighbours of a word: for each position, try all 26 letters; keep those that are in the set and not yet visited.
- The first time you dequeue
end, its depth is the shortest ladder length. BFS guarantees shortest-first.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach builds the graph explicitly: compare every pair of words, letter by letter, and add an edge whenever they differ in exactly one position; then run BFS from begin. It is correct, because BFS on that graph finds the shortest ladder. But the comparisons cost O(N² · L) for N words of length L, and a dense graph needs O(N²) space for its edges. At 5,000 words that is about 12.5 million pairs before the search even starts, almost all of them answering “no”. Enumerating every path with DFS and keeping the shortest is worse still: the number of paths can grow exponentially with the length of the ladder.
The approach
Treat each word as a node and each one-letter change as an edge, but never build the graph: generate a word's neighbors only when BFS reaches it. BFS explores in order of distance, so the first time end comes out of the queue, the depth stored with it is the shortest ladder. The invariant: every word in the queue was reached by a shortest path, and each word enters the queue at most once.
The reference puts words into wordset for constant-time lookups and returns 0 at once if end is not in it. The queue holds (word, depth) pairs, starting with (begin, 1) because the answer counts words, not steps. For each dequeued word, it tries every position i and every letter c, building nxt = word[:i] + c + word[i + 1:]; a candidate that is in wordset and not yet in seen is marked seen immediately and enqueued with depth + 1. Marking at enqueue time, not at dequeue time, is what keeps a word from being queued twice by two words on the same level.
Walk through an example
Trace the first example, ladder_length("hit", "cog", ["hot","dot","dog","lot","log","cog"]), one dequeue per row.
| step | dequeued (word, depth) | new neighbors enqueued | queue afterward |
|---|---|---|---|
| 1 | (hit, 1) | hot | [(hot, 2)] |
| 2 | (hot, 2) | dot, lot | [(dot, 3), (lot, 3)] |
| 3 | (dot, 3) | dog | [(lot, 3), (dog, 4)] |
| 4 | (lot, 3) | log | [(dog, 4), (log, 4)] |
| 5 | (dog, 4) | cog | [(log, 4), (cog, 5)] |
| 6 | (log, 4) | none: cog and dog are already seen | [(cog, 5)] |
| 7 | (cog, 5) | it is end | returns 5 |
Complexity
Optimal: O(N · 26 · L²) time, because at most N words are dequeued, each generates 26 · L candidates, and building and hashing a candidate costs O(L); O(N · L) space for wordset, seen and the queue. Brute force (pairwise graph, then BFS): O(N² · L) time and up to O(N²) space.
Edge cases to test
endnot in the list:ladder_length("hit", "cog", ["hot","dot","dog","lot","log"])→0, returned before any search.endin the list but unreachable:ladder_length("hit", "cog", ["hot", "cog"])→0, becausehotandcogdiffer in two letters and the queue runs dry.- One step:
ladder_length("a", "c", ["b", "c"])→2. The answer counts words, so a single change is 2, not 1. beginequal toend:ladder_length("hit", "hit", ["hit"])→1, but with an empty list it returns0, because the membership check onendruns first.- Words of other lengths in the list, such as
hittorco, are never generated as candidates, so they are ignored without any special case. - Letters outside
atoz: the loop only tries lowercase letters, soladder_length("hit", "hOt", ["hOt"])→0. That is why the constraints require lowercase words.
Common mistakes
- Using DFS — it explores deep first and does not give the shortest ladder; BFS does.
- Comparing every pair of words (O(N²·L)) instead of generating each word’s neighbors on demand or grouping words by wildcard patterns.
- Not marking words visited, so BFS revisits and can loop.
- Counting steps instead of words: starting the depth at 0 gives 4 for the first example instead of 5.
Follow-ups an interviewer asks next
- How would bidirectional BFS speed this up?
- Run two searches, one from
beginand one fromend, and always expand the smaller frontier by one level; stop when a newly generated word is already in the other side’s visited set. If each word has aboutbneighbors and the ladder hasdsteps, a one-sided search explores on the order ofb^dwords, while two halves explore about2 · b^(d/2). - How would you return the actual ladders (Word Ladder II)?
- Record parents during the BFS. For one shortest ladder, store the first parent of each word and walk back from
end. For all of them, process level by level and mark words visited only after the whole level finishes, so every same-level parent is kept, then enumerate paths backward. The number of shortest ladders can grow exponentially, so the output itself can be the bottleneck. - When are wildcard buckets better than trying 26 letters?
- Precompute a map from patterns like
h*tto the words matching them; a word’s neighbors are then the union of itsLbuckets. That costsO(N · L²)to build and avoids generating 26 candidates per position, which pays off when the alphabet is large, such as Unicode text, or when most letter substitutions are not words. - What if some letter changes cost more than others?
- Then the graph is weighted and BFS no longer finds the cheapest ladder: use Dijkstra’s algorithm with a priority queue keyed by cost. If every change costs at least 1, A* with the number of differing letters as the heuristic is admissible, because each step fixes at most one letter.
Where this shows up
Word ladder is BFS-for-shortest-path on a graph you generate on the fly — the same “fewest steps between states” pattern as puzzle solvers, refactor/transformation planners, and any unweighted shortest-path problem where you expand neighbors lazily rather than storing the whole graph.
Reference solution
Reference solution — Python
from collections import deque
def ladder_length(begin, end, words):
wordset = set(words)
if end not in wordset:
return 0 # unreachable by definition
queue = deque([(begin, 1)]) # (word, words-used-so-far)
seen = {begin}
while queue:
word, depth = queue.popleft()
if word == end:
return depth
for i in range(len(word)): # try changing each position
for c in "abcdefghijklmnopqrstuvwxyz":
nxt = word[:i] + c + word[i + 1:]
if nxt in wordset and nxt not in seen:
seen.add(nxt)
queue.append((nxt, depth + 1))
return 0
print(ladder_length("hit", "cog", ["hot","dot","dog","lot","log","cog"])) # -> 5
print(ladder_length("hit", "cog", ["hot","dot","dog","lot","log"])) # -> 0Reference solution — TypeScript
function ladderLength(begin: string, end: string, words: string[]): number {
const wordset = new Set(words);
if (!wordset.has(end)) return 0; // unreachable
const queue: [string, number][] = [[begin, 1]];
const seen = new Set([begin]);
const alphabet = "abcdefghijklmnopqrstuvwxyz";
while (queue.length) {
const [word, depth] = queue.shift()!;
if (word === end) return depth;
for (let i = 0; i < word.length; i++) { // change each position
for (const c of alphabet) {
const nxt = word.slice(0, i) + c + word.slice(i + 1);
if (wordset.has(nxt) && !seen.has(nxt)) {
seen.add(nxt);
queue.push([nxt, depth + 1]);
}
}
}
}
return 0;
}
console.log(ladderLength("hit", "cog", ["hot","dot","dog","lot","log","cog"])); // -> 5
console.log(ladderLength("hit", "cog", ["hot","dot","dog","lot","log"])); // -> 0Questions about Word Ladder
Why does Word Ladder use BFS instead of DFS?
Because the question asks for the shortest transformation, and every step costs the same. Breadth-first search explores all words one change away, then all words two changes away, and so on, so the first time it reaches the end word it has found a shortest ladder and can stop. Depth-first search follows one chain as far as it can go, so the first ladder it finds can be much longer than necessary, and proving that no shorter one exists means exploring every path, which can take exponential time on a large word list.
What is the time complexity of Word Ladder?
With BFS and on-the-fly neighbor generation, it is O(N times 26 times L squared) for N words of length L. At most N words are taken off the queue, each produces 26 candidates per letter position, so 26 times L candidates, and building and hashing each candidate string costs O(L). Space is O(N times L) for the word set, the visited set and the queue. Building the full graph first by comparing every pair of words costs O(N squared times L), which is far slower when the dictionary is large and the words are short.
Why mark a word visited when it is enqueued rather than when it is dequeued?
Because several words on the same BFS level can share a neighbor. If you only mark words when you take them off the queue, each of those words adds the shared neighbor again, so it sits in the queue several times and is expanded several times, which wastes work and on dense word lists can multiply the queue size. Marking a word the moment it is first discovered guarantees it is enqueued once, and since BFS discovers every word at its shortest distance first, nothing is lost by ignoring later discoveries.
How does bidirectional BFS speed up Word Ladder?
It searches from both ends at once and stops when the two searches meet in the middle. A one-sided search must explore every word within the full distance d of the start, and the number of such words grows roughly like the branching factor to the power d. Two searches that each go about half the distance explore roughly twice the branching factor to the power d over 2, which is dramatically smaller for long ladders. Expanding whichever frontier is currently smaller keeps the two sides balanced. The answer is still the same shortest ladder length.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- Top-K Frequent ElementsCount, then rank — the two-step pattern behind trending topics and hot-key detection. A hash map counts; a (-count, value) sort ranks with a clean tie-break; buckets get you to O(n) if you want the flex. Solve it in Python or TypeScript.Read →