Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°44

Word Ladder

Hard GraphsBFSStrings

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.

SOLVE IT YOURSELF

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.

YOUR TASK

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.

CPython · WebAssembly
Examples and constraints 5 examples
EXAMPLE 1
Input begin = 'hit', end = 'cog', words = ['hot','dot','dog','lot','log','cog']
Output 5
hit → hot → dot → dog → cog is 5 words
EXAMPLE 2
Input begin = 'hit', end = 'cog', words = ['hot','dot','dog','lot','log']
Output 0
end word "cog" is not in the list
EXAMPLE 3
Input begin = 'cat', end = 'dog', words = ['cot','cog','dog']
Output 4
cat → cot → cog → dog, one letter per step
EXAMPLE 4
Input begin = 'hit', end = 'cog', words = ['hot','cog']
Output 0
edge case: cog is in the list but unreachable, since hot and cog differ in two letters
EXAMPLE 5
Input begin = 'a', end = 'c', words = ['b','c']
Output 2
edge case: a single change is a ladder of two words
CONSTRAINTS
  • All words have the same length and are lowercase; begin need not be in words, but end must 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
HINTS — 4 IDEAS
  1. Put the word list in a set for O(1) membership. If end is not in it, the answer is 0 immediately.
  2. BFS level by level from begin, carrying the number of words used so far (start at 1).
  3. Neighbours of a word: for each position, try all 26 letters; keep those that are in the set and not yet visited.
  4. 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.

stepdequeued (word, depth)new neighbors enqueuedqueue 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 endreturns 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

  • end not in the list: ladder_length("hit", "cog", ["hot","dot","dog","lot","log"]) → 0, returned before any search.
  • end in the list but unreachable: ladder_length("hit", "cog", ["hot", "cog"]) → 0, because hot and cog differ 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.
  • begin equal to end: ladder_length("hit", "hit", ["hit"]) → 1, but with an empty list it returns 0, because the membership check on end runs first.
  • Words of other lengths in the list, such as hitt or co, are never generated as candidates, so they are ignored without any special case.
  • Letters outside a to z: the loop only tries lowercase letters, so ladder_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 begin and one from end, 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 about b neighbors and the ladder has d steps, a one-sided search explores on the order of b^d words, while two halves explore about 2 · 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*t to the words matching them; a word’s neighbors are then the union of its L buckets. That costs O(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"]))        # -> 0
Reference 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"]));       // -> 0

Questions 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.

Finished this one? 0 / 76 Challenges done
NextValid Parentheses Closest challenge to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges