Coding Interview Patterns Module 20 – Graph patterns: traversal
BFS over states: word ladders and padlocks
Solve word ladders and padlock puzzles with BFS over states: encode each state, find neighbours with wildcard buckets, and search from both ends at once.
What you will learn
- Encode puzzle positions as hashable values or as integers
- Generate neighbouring states with buckets of patterns or with substitutions
- Find the fewest moves between two states with breadth-first search
- Rebuild one shortest sequence of moves, or list every shortest one
- Search from both ends at once with bidirectional BFS
Before you start
What you’ll learn · Before you start
What you will learn
- Encode puzzle positions as hashable values or as integers
- Generate neighbouring states with buckets of patterns or with substitutions
- Find the fewest moves between two states with breadth-first search
- Rebuild one shortest sequence of moves, or list every shortest one
- Search from both ends at once with bidirectional BFS
Before you start
On this page
A word ladder turns one word into another by changing one letter at a time, with every word along the way taken from a list, and asks for the fewest changes. It is a shortest-path problem on a graph that nobody draws: every word is a node, two words are joined when they differ in exactly one position, and a breadth-first search from the start word finds the answer. The same BFS over states solves padlocks, jugs, sliding tiles and board moves, whenever a position can be written down as a value and every move costs the same.
The problem: a ladder of four-letter words
The problem
You are given a start word, a target word and a list of words, all lower-case and all of the same length L. A
ladder begins with start, ends with target, and each word in it differs from the word before in exactly one
position; every word after start must come from the list. Return the number of changes in the shortest ladder,
0 when start equals target, and −1 when no ladder exists. Limits: 1 ≤ L ≤ 10 and up to 50,000 words.
The lesson’s list has 32 words, from lamp and lame to kind and zinc. Three examples:
- From
lamptofire: 4 changes, throughlame,fameandfare. - From
lamptokind: 6 changes, for example throughlame,lime,line,fineandfind. - From
lamptozinc: −1, because no word of the list differs fromzincin a single letter.
The word list in layers, as BFS reaches it from lamp
Text description of the diagram
Every word that a breadth-first search reaches from "lamp" within 4 changes is drawn in the row of its distance, with a thin line to the word it was first reached from.
- Row 0: lamp.
- Row 1: lame and limp, both from lamp.
- Row 2: fame, lime, came, lane and game, all from lame.
- Row 3: fare and fate from fame; line from lime; cane from came; mane from lane; gate and gale from game.
- Row 4: fire from fare; fine, mine and wine from line; male from mane.
The ladder lamp, lame, fame, fare, fire is drawn with thick lines and shaded words: 4 changes, the fewest. The search reaches 20 words by row 4; of the other 12 words of the list, 11 are farther away and zinc is never reached at all.
The brute force: every ladder, and every pair of words
The first idea is to walk every ladder that repeats no word, depth first, and keep the shortest. The run counts them on the lesson’s list and on two smaller lists inside it:
"""Every ladder from lamp to fire that repeats no word, found depth first: the count explodes as the list grows."""
WORDS = ["lamp", "lame", "lime", "limp", "line", "fine", "fire", "fare", "fame", "came", "cane", "lane", "mane",
"mine", "mire", "wire", "wine", "wise", "rise", "rite", "fate", "gate", "game", "gale", "male", "mile",
"mild", "wild", "wind", "kind", "find", "zinc"]
def one_change(a, b):
return sum(x != y for x, y in zip(a, b)) == 1
def every_ladder(words, start, target):
"""(number of ladders, fewest changes, calls made): a depth-first walk over ladders without repeats."""
nearby = {w: [v for v in words if one_change(w, v)] for w in set(words) | {start}}
ladders, fewest, calls = 0, None, 0
on_ladder = {start}
def walk(word, changes):
nonlocal ladders, fewest, calls
calls += 1
if word == target:
ladders += 1
fewest = changes if fewest is None else min(fewest, changes)
return
for nxt in nearby[word]:
if nxt not in on_ladder:
on_ladder.add(nxt)
walk(nxt, changes + 1)
on_ladder.discard(nxt)
walk(start, 0)
return ladders, fewest, calls
print("words in the list ladders from lamp to fire fewest changes calls")
for size in (22, 27, 32):
keep = WORDS if size == 32 else [w for w in WORDS if w not in (
("wise", "rise", "rite", "gate", "fate") + (("mane", "cane", "came", "game", "gale") if size == 22 else ()))]
ladders, fewest, calls = every_ladder(keep, "lamp", "fire")
print(f"{len(keep):>15} {ladders:>27,} {fewest:>16} {calls:>9,}")
n = 50_000
print(f"comparing every pair of {n:,} words to find the neighbours: {n * (n - 1) // 2:,} pairs") Output
words in the list ladders from lamp to fire fewest changes calls
22 599 4 3,087
27 26,091 4 152,822
32 146,590 4 993,929
comparing every pair of 50,000 words to find the neighbours: 1,249,975,000 pairs
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ladders_brute.py
Five more words multiply the count: 599 ladders on 22 words, 26,091 on 27 and 146,590 on 32, almost a million calls for a list that fits on a postcard. Even a search that visits each word once has a second cost hidden in it: finding a word’s neighbours by comparing it with every other word costs 1,249,975,000 pair comparisons on 50,000 words.
What to notice
The graph does not have to exist before the search. Each word’s neighbours can be produced when the search needs
them: change each of the L positions to each other letter and look the result up in a set, or file every word under
the L patterns that blank one of its positions, such as l*mp, so that the words sharing a pattern are exactly the
neighbours through that position. Then the search touches each word once, and the fewest changes is the BFS distance.
Padlocks work the same way: each code is a node, and turning one wheel one step up or down gives its neighbours.
Neighbours on demand: three ways, counted Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- The search with buckets, each read once
- The code in five languages
- Every shortest ladder, not just one
- Bidirectional BFS: two smaller searches
- The padlock: codes as numbers
- Variants
- Edge cases and common bugs
- Exercise: fewest turns to open a padlock
- Fewest turns to open a padlock
- A practice set with answers
- Check yourself
- Interview questions
- In a BFS over states, what must one state contain, and how can you tell that something is missing from it?
- A word-ladder service must answer queries on a dictionary of 200,000 words of up to 12 letters, each within 50 ms. A BFS that compares every word with every other word is far too slow. Design the neighbour lookup, estimate the work per query, and say what you build in advance.
- You must return every shortest ladder between two words. A first version keeps a whole partial ladder in the queue for every path it extends, and runs out of memory on a large dictionary. Why does that happen, and how do you fix it?
- When does bidirectional BFS pay off, and when does it not? Estimate the saving on a graph where every state has 10 neighbours and the target is 8 moves away, then on a 4-wheel padlock, and name a case where it cannot be used.
- Key takeaways
7 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 21 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- breadth-first search (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- collections: defaultdict objects (The Python Standard Library) (Python Software Foundation)
- Time complexity of operations on built-in types (Python Software Foundation)
- HashMap (Java SE 25 and JDK 25 API) (Oracle)
- std::unordered_map (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: Deletion of map elements (The Go Authors)
- Map (JavaScript reference) (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress