Learn how to find all shortest paths (Word Ladder II) from one word to another by changing one letter at a time. This guide uses a clear example, and in-place code comments to illustrate a level-by-level BFS approach.
Here’s the [Problem Link] to begin with.
What the Problem Asks
You have a beginWord, an endWord, and a list of words called wordList.
You want to transformbeginWord
intoendWord
by changing exactly one letter at a time.
Each new intermediate word must be inwordList
.
Return all possible shortest sequences of words frombeginWord
toendWord
.
Example 1
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]
Shortest sequences:
["hit","hot","dot","dog","cog"]
["hit","hot","lot","log","cog"]
Example 2
beginWord = "a"
endWord = "c"
wordList = ["a","b","c"]
Shortest sequences:
["a","c"]
(“a” → “c” is valid because “c” is in the list, and we changed one letter.)
Also read about the Python Program to Solve Word Ladder – 1.
Intuition & Approach
- We want all shortest sequences, not just one.
- BFS layer by layer naturally finds the shortest distance.
- At each step, we keep whole sequences in the queue (not just words).
- We also remove words from the set once they appear at the current level, so later levels don’t reuse them and produce longer paths.
In very simple terms:
- Start with a queue of lists, each list is a transformation path so far.
- In each iteration, we pop a path, look at its last word, generate all possible “one-letter” changes.
- If a changed word is in
wordSet
, we make a new path by appending that word and push it into the queue. - As soon as we see a path ending in
endWord
, we record it—because BFS guarantees this is the shortest length. - We keep collecting all such paths until we finish processing the current BFS layer.
- Finally, we return all collected shortest paths.
Code
from collections import deque
class Solution:
def findLadders(
self, beginWord: str, endWord: str, wordList: List[str]
) -> List[List[str]]:
# 1. Put all words into a set for fast lookups
wordSet = set(wordList)
# If the endWord is not in our list, we can’t transform to it
if endWord not in wordSet:
return []
result = [] # To store all shortest sequences
queue = deque() # BFS queue of paths (lists of words)
queue.append([beginWord]) # Start with the path ["beginWord"]
while len(queue) != 0:
level_size = len(queue)
chosen_words = set() # Words we will remove after this layer
# Process everything in the current layer
for _ in range(level_size):
sequence = queue.popleft() # A path so far
last_word = sequence[-1] # The most recent word
# If we already reached endWord, record this path
if last_word == endWord:
result.append(sequence)
# Don’t continue expanding this path; it’s complete
continue
# Try changing each letter of last_word
for i in range(len(last_word)):
for ch in "abcdefghijklmnopqrstuvwxyz":
# Skip if the letter is the same
if ch == last_word[i]:
continue
# Form a new word by replacing the i-th letter
new_word = last_word[:i] + ch + last_word[i + 1:]
# If this new word is in our set, it’s valid
if new_word in wordSet:
# Make a new path by appending new_word
new_seq = sequence + [new_word]
queue.append(new_seq)
# Mark new_word for removal after this layer
chosen_words.add(new_word)
# Remove all words that were used in this layer
for w in chosen_words:
wordSet.remove(w)
return result
How the Code Works, Step by Step
- wordSet = set(wordList)
- We need to quickly check if a candidate word is allowed. A Python set does that in O(1).
- If endWord not in wordSet, return []
- If you can’t ever reach the target word, answer is empty.
- Initialize
result = []
will hold all shortest paths we find.queue
holds paths. Start with just[beginWord]
.
- While queue is not empty
level_size = len(queue)
captures how many paths are in this BFS level.chosen_words = set()
to track which new words we used in this level.
- Process each path in the current BFS level
sequence = queue.popleft()
: get the next path.last_word = sequence[-1]
: look at the last word in that path.- If
last_word == endWord
, we found a complete shortest path. Append it toresult
and do not extend it further (because BFS guarantees this is minimal length). - Otherwise, generate neighbors by changing one letter at a time:
- For each position
i
inlast_word
, try every lettera–z
. - Skip if the letter is unchanged.
- Form
new_word
. - If
new_word
is inwordSet
, it’s a valid next step:new_seq = sequence + [new_word]
builds a new path list.- Enqueue
new_seq
. - Add
new_word
tochosen_words
so that at the end of this layer we remove it fromwordSet
.
- For each position
- After finishing this entire BFS layer, remove all
chosen_words
fromwordSet
.- This prevents paths in deeper layers (longer paths) from reusing the same words and ensures we only collect shortest sequences.
- Return
result
- All recorded paths in
result
have length equal to the shortest transformation count.
- All recorded paths in
Dry Run with Example
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]
- Initialize:
wordSet = {"hot","dot","dog","lot","log","cog"}
queue = [["hit"]]
.
- Layer 1 (size=1):
- Sequence:
["hit"]
, last_word=”hit” - Generate neighbors of “hit”:
- Change “h”→“a”..“z” except “h”; only valid is “hot”.
- New path
["hit","hot"]
, queue it. chosen_words = {"hot"}
.- End layer, remove “hot” from
wordSet
.
- Sequence:
- Layer 2 (size=1):
- Sequence:
["hit","hot"]
, last_word=”hot” - Generate neighbors for “hot”:
- “dot” and “lot” are valid (in
wordSet
).
- “dot” and “lot” are valid (in
- Enqueue
["hit","hot","dot"]
and["hit","hot","lot"]
. chosen_words = {"dot","lot"}
.- Remove them from
wordSet
.
- Sequence:
- Layer 3 (size=2):
- Sequence
["hit","hot","dot"]
, last_word=”dot”- Valid neighbor: “dog” → enqueue
["hit","hot","dot","dog"]
.
- Valid neighbor: “dog” → enqueue
- Sequence
["hit","hot","lot"]
, last_word=”lot”- Valid neighbor: “log” → enqueue
["hit","hot","lot","log"]
.
- Valid neighbor: “log” → enqueue
chosen_words = {"dog","log"}
.- Remove “dog” and “log” from
wordSet
.
- Sequence
- Layer 4 (size=2):
- Sequence
["hit","hot","dot","dog"]
, last_word=”dog”- Valid neighbor: “cog” → enqueue
["hit","hot","dot","dog","cog"]
.
- Valid neighbor: “cog” → enqueue
- Sequence
["hit","hot","lot","log"]
, last_word=”log”- Valid neighbor: “cog” → enqueue
["hit","hot","lot","log","cog"]
.
- Valid neighbor: “cog” → enqueue
chosen_words = {"cog"}
.- Remove “cog” from
wordSet
.
- Sequence
- Layer 5 (size=2):
- Sequence
["hit","hot","dot","dog","cog"]
, last_word=”cog” → matchesendWord
, record it inresult
. - Sequence
["hit","hot","lot","log","cog"]
, last_word=”cog” → record it inresult
.
- Sequence
- Return
result = [ ["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"] ]
.
Time & Space Complexity
- Time Complexity:
- BFS can enqueue up to O(N × L) paths in the worst case, where N = number of words and L = word length.
- Each path expansion tries L positions × 26 letters → O(26 × L) checks per path.
- Overall roughly O(N × L × 26), which is acceptable for moderate input sizes.
- Space Complexity:
- Storing all partial paths and a word set → O(N × L) in total.
Conclusion
By using a level-by-level BFS where each queue entry is a full transformation path, we automatically find the shortest sequences. Removing each new word from the set at the end of its layer ensures we never revisit longer paths. This simple method collects all shortest paths in a clean, easy-to-follow way. Happy coding!
For any changes to the article, kindly email at code@codeanddebug.in or contact us at +91-9712928220.