BFS on an implicit graph — shortest path without any explicit edges.
Given a beginWord, an endWord, and a dictionary word list, find the length of the shortest transformation sequence from begin to end where:
# beginWord="hit", endWord="cog"
# wordList=["hot","dot","dog","lot","log","cog"]
# hit → hot → dot → dog → cog → 5 steps
#
# beginWord="hit", endWord="cog"
# wordList=["hot","dot","dog","lot","log"]
# "cog" not in list → 0 (impossible)
Each word is a node. Two words are connected by an edge if they differ by exactly one letter. We want the shortest path from beginWord to endWord — that's BFS.
The clever part: instead of precomputing all edges, we generate neighbors on the fly by trying all 26 letters at each position.
# From "hot", try changing each character:
# _ot: aot, bot, cot, dot ← in wordList! hot, jot, lot ← in list!
# h_t: hat, hbt, ... (only those in wordList matter)
# ho_: hoa, hob, ... (only those in wordList matter)
from collections import deque
def ladder_length(beginWord, endWord, wordList):
word_set = set(wordList)
if endWord not in word_set:
return 0
queue = deque([(beginWord, 1)]) # (current_word, steps_taken)
visited = {beginWord}
while queue:
word, steps = queue.popleft()
for i in range(len(word)):
for ch in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + ch + word[i+1:]
if next_word == endWord:
return steps + 1
if next_word in word_set and next_word not in visited:
visited.add(next_word)
queue.append((next_word, steps + 1))
return 0 # No path found
print(ladder_length("hit", "cog", ["hot","dot","dog","lot","log","cog"])) # 5
| Property | Value |
|---|---|
| Time | O(M² × N) where M = word length, N = number of words |
| Space | O(M × N) for the queue and visited set |
wordList to a set is critical. Checking membership in a list is O(n); in a set it's O(1). For a large dictionary this makes an enormous difference.# Start: queue=[("hit",1)], visited={"hit"}
#
# Pop ("hit",1). Try all 1-letter changes:
# "hot" → in word_set, not visited → enqueue ("hot",2)
# queue=[("hot",2)]
#
# Pop ("hot",2). Try all changes:
# "dot" → enqueue ("dot",3)
# "lot" → enqueue ("lot",3)
# queue=[("dot",3),("lot",3)]
#
# Pop ("dot",3).
# "dog" → enqueue ("dog",4)
#
# Pop ("lot",3).
# "log" → enqueue ("log",4)
#
# Pop ("dog",4).
# "cog" == endWord! Return 4+1 = 5 ✓
Search from both ends simultaneously. When the two frontiers meet, add their step counts. This dramatically reduces the search space — instead of exploring a sphere of radius d, you explore two spheres of radius d/2, which is much smaller.
def ladder_length_bidir(beginWord, endWord, wordList):
word_set = set(wordList)
if endWord not in word_set: return 0
front, back = {beginWord}, {endWord}
visited = {beginWord, endWord}
steps = 1
while front:
if len(front) > len(back):
front, back = back, front # Always expand the smaller frontier
next_front = set()
for word in front:
for i in range(len(word)):
for ch in 'abcdefghijklmnopqrstuvwxyz':
nw = word[:i] + ch + word[i+1:]
if nw in back: return steps + 1
if nw in word_set and nw not in visited:
next_front.add(nw)
visited.add(nw)
front = next_front
steps += 1
return 0