Word ladderLESSON 2.23 · 23 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 49 of 252
LESSON 2.23 · 23 OF 43 IN CHAPTERTry it, then open the solution

Word ladder

THE PROBLEM

“A word-game editor changes one letter per move. Each intermediate word must be in our dictionary. Players need the fewest moves between two words, including a path they can inspect. How would you distinguish impossible from zero moves?”

Write this:

def word_ladder(start, end, words):
    ...  # Return endpoint-inclusive shortest path, or []; words are lowercase ASCII.

Constructed practice question. Prerequisite: BFS. A graph may be implicit: its edges are generated from the current word rather than stored. Breadth-first search visits states by increasing number of moves.

Contract Required behavior
Input Equal positive-length lowercase ASCII endpoints and dictionary words
Output A shortest endpoint-inclusive list, or [] when impossible
Membership Start need not be in dictionary; a different end must be
Boundaries Equal endpoints return [start]; repeated dictionary entries collapse
Failure/scope Invalid lengths/characters raise ValueError; no insertions or deletions
Optional refresher · the underlying tool

A word ladder is a shortest-path problem: two words are adjacent only when exactly one position differs. BFS explores transformations in increasing number of changes:

from collections import deque
queue = deque([("cat", 0)])
visited = {"cat"}  # mark on enqueue so a word is not queued twice

If the dictionary contains cat, cot, cog, dog, then cat → cot → cog → dog uses three changes. Say whether the count includes words or edges.

A design choice worth saying aloud

The queue holds (word, number_of_changes) and visited marks words on enqueue, preventing two parents from scheduling the same state. This is correct because all transformations cost one change; weighted edits would require a different frontier. Define whether length means words or changes before reporting 3 for cat → cot → cog → dog.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: A shortest endpoint-inclusive list, or [] when impossible.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
start hit, end cog, words {hot,dot,dog,lot,log,cog}
Expected result
[hit,hot,dot,dog,cog] or the equally short path via lot,log

What it is testing: Four changes, five returned words.

02 · Same endpoint

Input / starting state
same to same
Expected result
["same"]

What it is testing: Zero transformations still includes the endpoint.

03 · Missing end

Input / starting state
hit to cog, words {hot,dot,dog}
Expected result
[]

What it is testing: A different end must be admitted.

04 · Unreachable

Input / starting state
hit to cog, words {hot,cog}
Expected result
[]

What it is testing: Both are valid words but not connected.

05 · Duplicates

Input / starting state
dictionary repeats a word
Expected result
same path semantics

What it is testing: Repeated entries do not create states.

06 · Invalid

Input / starting state
mixed lengths or non-lowercase ASCII
Expected result
ValueError

What it is testing: Neighbor generation depends on the alphabet contract.

For each case, show which branch or state change produces that result.

hit → cog using {hot,dot,dog,lot,log,cog} has four moves, with answer [hit,hot,dot,dog,cog]. The equally short route through lot/log is also valid. With only {hot,cog}, return []. Clarify whether all shortest paths are needed; returning one predecessor per word deliberately answers only one-path queries.

Diagram: Test-case scenarios to settle before coding

Try an independent implementation. Explain when a word becomes visited and whether two parents are allowed to enqueue it before you open the solution.

Solution, representation choice, and follow-ups

The direct baseline compares every pair of dictionary words and stores an edge when exactly one character differs. For N words of length L this costs O(N²L), even if the reachable component is tiny. DFS can find a route but its first route need not be shortest: exploration order can follow a long detour first.

Instead generate each one-character replacement on demand. Check membership in a set, mark the word discovered immediately, and save its parent. The BFS queue contains the current distance layer followed by the next. Invariant: the first discovery of a word fixes its minimum move count, because every generating parent has the smallest unprocessed distance. Marking only on removal permits duplicate queue entries and redundant parent assignment.

Removed word Newly discovered Saved parent evidence
hit hot hot ← hit
hot dot, lot both ← hot
dot, lot dog, log dog ← dot; log ← lot
dog cog cog ← dog; reconstruct backward

The fixed 26-letter alphabet does not make Python string creation free. Each of 26L candidates costs O(L) to construct/hash, giving O(NL²) search time in the worst case, plus O(NL) input validation. Stored dictionary and discovered strings use O(NL) space; parent/queue entries add O(N). The returned path uses O(PL) if its strings are counted, with P words. No quadratic adjacency table is retained.

Follow-up 1 — return every shortest path. Predict what is lost when cog stores only dog. Save all parents at the preceding depth, finish the entire winning layer, and traverse the resulting parent DAG. Output may be exponential; report output-sensitive bounds rather than promising linear total work.

Diagram: Test-case scenarios to settle before coding

Follow-up 2 — large dictionary, one pair. Bidirectional BFS expands the smaller frontier from start/end and joins compatible layers. Keep separate visited/depth maps and reconstruct both halves. It can reduce explored states but does not remove the need to account for neighbor generation or prove the stopping rule.

Senior depth is deriving the implicit graph and proving shortestness. Lead depth adds dictionary versioning and query-memory limits if this becomes a service.

Reference: solution.py (download file, source below); tests validate actual edges, optimal length, unreachable endpoints, identity, duplicates, and malformed words.

solution.py · solution.py
"""Shortest one-character transformation using an implicit unweighted graph."""
from collections import deque
from string import ascii_lowercase


def word_ladder(start, end, words):
    """Return endpoint-inclusive shortest path, or []; words are lowercase ASCII."""
    words = set(words)
    if not start or len(start) != len(end):
        raise ValueError("endpoints must have equal positive length")
    if any(len(w) != len(start) or any(c not in ascii_lowercase for c in w)
           for w in words | {start, end}):
        raise ValueError("words must have equal length and lowercase ASCII letters")
    if start == end:
        return [start]
    if end not in words:
        return []
    parent = {start: None}
    queue = deque([start])
    while queue:
        word = queue.popleft()
        for i in range(len(word)):
            for char in ascii_lowercase:
                if char == word[i]:
                    continue
                candidate = word[:i] + char + word[i + 1:]
                if candidate in words and candidate not in parent:
                    parent[candidate] = word
                    if candidate == end:
                        path = [end]
                        while parent[path[-1]] is not None:
                            path.append(parent[path[-1]])
                        return path[::-1]
                    queue.append(candidate)
    return []
python -m unittest discover -s curriculum/01-code/02-data-structures-algorithms/problems/22-word-ladder -p 'test_*.py'