Intuition
Two words are neighbours (one-letter apart) iff they match if you mask out one position with *. So we precompute, for every word and every position, the pattern that results from masking that position — and bucket all words by their patterns. Now finding all neighbours of a word costs O(M) lookups, not O(N) scans. BFS finds shortest paths, so the first time we reach endWord gives the minimum ladder length.
Explanation
Why patterns are faster than pairwise comparison
Naive: for each word, compare with every other word and count differing letters. That's O(N · N · M) = O(N² M). Too slow for N up to a few thousand.
Pattern bucketing: each M-letter word produces M patterns (one per position, replace that letter with *). Two words are neighbours iff they share a pattern. So we precompute the buckets once (O(N · M²) time, since each pattern is O(M) to construct), and then each neighbour-lookup is O(M) patterns × constant per bucket.
Steps
- Convert
wordListto a set. IfendWordisn't in the set, no ladder exists — return0. - Build pattern buckets: for every word
wand every indexi, addwtobuckets[w[:i] + '*' + w[i+1:]]. - BFS: start with
(beginWord, 1). MarkbeginWordas visited. - While the queue is non-empty:
- Pop
(word, steps). - For each
iin0..M-1: compute the patternword[:i] + '*' + word[i+1:]. For eachnxtinbuckets[pattern]:- If
nxt == endWord, returnsteps + 1. - Else if
nxtnot invisited, mark visited and enqueue(nxt, steps + 1).
- If
- Optimisation: clear
buckets[pattern]after processing — every word in that bucket has been or is now in the BFS frontier, so revisiting the bucket later wastes work.
- Pop
- If BFS exhausts without finding
endWord, return0.
Walk-through
beginWord = "hit", endWord = "cog", wordList = ["hot", "dot", "dog", "lot", "log", "cog"].
Buckets after preprocessing (showing relevant ones):
| pattern | words |
|---|---|
*ot | hot, dot, lot |
h*t | hot, hit (added when we BFS-process "hit") |
ho* | hot |
*og | dog, log, cog |
d*t | dot |
do* | dot, dog |
l*t | lot |
lo* | lot, log |
c*g | cog |
BFS:
| pop | patterns checked | neighbours found | enqueued |
|---|---|---|---|
(hit, 1) | *it, h*t, hi* | hot (via h*t) | (hot, 2) |
(hot, 2) | *ot, h*t, ho* | dot, lot | (dot, 3), (lot, 3) |
(dot, 3) | *ot, d*t, do* | dog | (dog, 4) |
(lot, 3) | *ot, l*t, lo* | log | (log, 4) |
(dog, 4) | *og, d*g, do* | cog (== endWord) | return 5 ✓ |
The shortest ladder is hit → hot → dot → dog → cog — 5 words.
Why BFS gives the shortest ladder
BFS explores nodes in non-decreasing distance order. The first time we touch endWord, the current steps + 1 is the shortest possible (any other path would have been popped earlier with a higher distance).
Edge cases
endWordnot inwordList: the early return.beginWord == endWord: the loop popsbeginWordand finds it as a neighbour-of-itself in some bucket. Return2if it's in the list, else handle as no path. (Most variants assume distinct words.)- Disconnected
endWord: BFS exhausts; return0.
Complexity
- Time:
O(M² · N)— building buckets isO(N · M²)(each pattern isO(M)to build,Mpatterns per word). BFS visits each word once and inspectsMpatterns per word, each pattern triggersO(M)work to construct + walking the bucket. - Space:
O(M² · N)for the buckets.
Implementation
import java.util.*;
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> words = new HashSet<>(wordList);
if (!words.contains(endWord)) return 0;
int M = beginWord.length();
Map<String, List<String>> buckets = new HashMap<>();
for (String w : words) {
for (int i = 0; i < M; i++) {
String pattern = w.substring(0, i) + "*" + w.substring(i + 1);
buckets.computeIfAbsent(pattern, k -> new ArrayList<>()).add(w);
}
}
Set<String> visited = new HashSet<>();
visited.add(beginWord);
Deque<String> queue = new ArrayDeque<>();
queue.add(beginWord);
int steps = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int s = 0; s < size; s++) {
String word = queue.poll();
for (int i = 0; i < M; i++) {
String pattern = word.substring(0, i) + "*" + word.substring(i + 1);
List<String> neighbors = buckets.getOrDefault(pattern, Collections.emptyList());
for (String nxt : neighbors) {
if (nxt.equals(endWord)) return steps + 1;
if (!visited.contains(nxt)) {
visited.add(nxt);
queue.add(nxt);
}
}
buckets.put(pattern, Collections.emptyList());
}
}
steps++;
}
return 0;
}
}