Word Ladder
Difficulty: Hard Source: NeetCode
Problem
A transformation sequence from word
beginWordto wordendWordusing a dictionarywordListis a sequence of wordsbeginWord -> s1 -> s2 -> ... -> sksuch that:
- Every adjacent pair of words differs by a single letter.
- Every
sifor1 <= i <= kis inwordList. Note thatbeginWorddoes not need to be inwordList.sk == endWordGiven two words,
beginWordandendWord, and a dictionarywordList, return the number of words in the shortest transformation sequence frombeginWordtoendWord, or0if no such sequence exists.Example 1: Input:
beginWord = "hit",endWord = "cog",wordList = ["hot","dot","dog","lot","log","cog"]Output:5(sequence: hit → hot → dot → dog → cog)Example 2: Input:
beginWord = "hit",endWord = "cog",wordList = ["hot","dot","dog","lot","log"]Output:0(cog not in wordList)Constraints:
1 <= beginWord.length <= 10endWord.length == beginWord.length1 <= wordList.length <= 5000- All words have the same length and consist of lowercase English letters
Prerequisites
Before attempting this problem, you should be comfortable with:
- BFS for shortest path — minimum transformation steps = shortest path in a graph
- State-space search — each word is a node; edges connect words differing by one letter
- Set lookups — converting wordList to a set for O(1) neighbor checking
1. Brute Force BFS (Generate all neighbors by comparison)
Intuition
BFS from beginWord. At each step, find all words in wordList that differ by exactly one letter from the current word. This is correct but O(N * L) per node for neighbor generation, where N = wordList size and L = word length.
Algorithm
- Convert
wordListto a set for fast lookup. - BFS with
(current_word, steps). Start at(beginWord, 1). - For each current word, check every word in the word set that differs by one letter.
- Return
stepswhenendWordis reached.
Solution
from collections import deque
def ladderLength(beginWord: str, endWord: str, wordList: list[str]) -> int:
word_set = set(wordList)
if endWord not in word_set:
return 0
def neighbors(word):
result = []
for candidate in word_set:
diff = sum(a != b for a, b in zip(word, candidate))
if diff == 1:
result.append(candidate)
return result
visited = {beginWord}
queue = deque([(beginWord, 1)])
while queue:
word, steps = queue.popleft()
for neighbor in neighbors(word):
if neighbor == endWord:
return steps + 1
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, steps + 1))
return 0
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log","cog"])) # 5
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log"])) # 0
Complexity
- Time:
O(N² * L)— for each of N nodes, compare against all N words - Space:
O(N)
2. BFS with Letter-by-Letter Neighbor Generation
Intuition
Instead of comparing every word in the list, generate candidate neighbors by replacing each letter of the current word with 'a' through 'z' and checking if the result is in the word set. This generates at most L * 26 candidates per word — much faster than scanning all N words.
Algorithm
- Convert
wordListto a set. - BFS from
beginWord. For each word, generate neighbors by replacing each of L positions with each of 26 letters. - Keep only neighbors that are in
word_setand not yet visited. - Return step count when
endWordis reached, or 0 if queue empties.
Solution
from collections import deque
def ladderLength(beginWord: str, endWord: str, wordList: list[str]) -> int:
word_set = set(wordList)
if endWord not in word_set:
return 0
visited = {beginWord}
queue = deque([(beginWord, 1)])
while queue:
word, steps = queue.popleft()
if word == endWord:
return steps
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
if c == word[i]:
continue
candidate = word[:i] + c + word[i+1:]
if candidate in word_set and candidate not in visited:
visited.add(candidate)
queue.append((candidate, steps + 1))
return 0
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log","cog"])) # 5
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log"])) # 0
print(ladderLength("a", "c", ["a","b","c"])) # 2
Complexity
- Time:
O(N * L * 26)— for each of N words in the BFS, try L positions × 26 letters - Space:
O(N * L)for visited set and queue
3. Bidirectional BFS
Intuition
Same as the Open the Lock trick: expand from both beginWord and endWord simultaneously. When the two frontiers collide, we’ve found the shortest path. This cuts the search space roughly in half in practice.
Solution
from collections import deque
def ladderLength(beginWord: str, endWord: str, wordList: list[str]) -> int:
word_set = set(wordList)
if endWord not in word_set:
return 0
def get_neighbors(word):
result = []
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
if c != word[i]:
candidate = word[:i] + c + word[i+1:]
if candidate in word_set:
result.append(candidate)
return result
begin_visited = {beginWord: 1}
end_visited = {endWord: 1}
begin_queue = deque([beginWord])
end_queue = deque([endWord])
while begin_queue and end_queue:
# Expand begin frontier
word = begin_queue.popleft()
steps = begin_visited[word]
for neighbor in get_neighbors(word):
if neighbor in end_visited:
return steps + end_visited[neighbor]
if neighbor not in begin_visited:
begin_visited[neighbor] = steps + 1
begin_queue.append(neighbor)
# Expand end frontier
word = end_queue.popleft()
steps = end_visited[word]
for neighbor in get_neighbors(word):
if neighbor in begin_visited:
return steps + begin_visited[neighbor]
if neighbor not in end_visited:
end_visited[neighbor] = steps + 1
end_queue.append(neighbor)
return 0
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log","cog"])) # 5
print(ladderLength("hit", "cog", ["hot","dot","dog","lot","log"])) # 0
Complexity
- Time:
O(N * L * 26)with smaller constant due to bidirectional search - Space:
O(N * L)
Common Pitfalls
Checking endWord in wordList. Unlike beginWord, endWord must be in the word list. If it’s not, immediately return 0.
Counting the begin word. The problem asks for the number of words in the sequence, not the number of steps. The sequence hit → hot → dot → dog → cog has 5 words but 4 steps. Start BFS with steps = 1 (counting the starting word) or remember to add 1 at the end.
Marking visited before enqueuing. Add words to visited when enqueuing, not when dequeuing. Otherwise, the same word can be enqueued multiple times from different paths, causing redundant work or incorrect results.