126. Word Ladder II
Problem Description
This problem asks you to find all the shortest transformation sequences between two words by changing one letter at a time, where each intermediate word must exist in a given word list.
Given:
beginWord
: The starting wordendWord
: The target wordwordList
: A list of valid words that can be used as intermediate steps
You need to find all possible shortest paths from beginWord
to endWord
following these rules:
-
Single Letter Change: You can only change one letter at a time to transform from one word to the next. For example, "hit" → "hot" changes only the second letter.
-
Valid Words Only: Every intermediate word in the transformation must exist in
wordList
. ThebeginWord
doesn't need to be in the list, but all other words (includingendWord
) must be present. -
Shortest Paths: You must return only the transformation sequences with the minimum number of steps. If multiple paths exist with the same minimum length, return all of them.
The output should be a list of lists, where each inner list represents one complete transformation sequence from beginWord
to endWord
.
For example:
- If
beginWord = "hit"
,endWord = "cog"
, andwordList = ["hot","dot","dog","lot","log","cog"]
- One possible shortest path could be:
["hit","hot","dot","dog","cog"]
- Another could be:
["hit","hot","lot","log","cog"]
- Both have the same length (5 words), so both should be returned
If no valid transformation sequence exists (like when endWord
is not in wordList
), return an empty list.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph where each word is a node, and edges exist between words that differ by exactly one letter. We need to find paths from
beginWord
toendWord
.
Is it a tree?
- No: The word transformation graph is not a tree - it can have cycles and multiple paths between nodes. For example, "hit" → "hot" → "dot" → "dog" and "hit" → "hot" → "lot" → "dog" both lead to "dog".
Is the problem related to directed acyclic graphs?
- No: The graph can have cycles (you could potentially transform back to a previous word through a different path).
Is the problem related to shortest paths?
- Yes: We need to find ALL shortest transformation sequences from
beginWord
toendWord
. The key requirement is finding paths with the minimum number of steps.
Is the graph weighted?
- No: Each transformation (edge) has the same cost - changing one letter is always one step. All edges have equal weight of 1.
BFS
- Yes: Since we're looking for shortest paths in an unweighted graph, BFS is the appropriate algorithm. BFS explores nodes level by level, guaranteeing that when we first reach the
endWord
, we've found the shortest path length. We can then track all paths of this minimum length.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the shortest transformation sequences. BFS is ideal here because:
- It finds the shortest path in unweighted graphs
- It can explore all nodes at the same distance simultaneously
- We can track multiple paths of the same minimum length by recording predecessors at each level
Intuition
The key insight is that we need to find all shortest paths, not just one. This makes the problem more complex than a standard BFS shortest path problem.
Think of the word transformation as exploring a maze where each room (word) has doors to other rooms (words that differ by one letter). We want to find all the shortest routes from the entrance to the exit.
The challenge is that BFS typically stops when it first reaches the destination, giving us one shortest path. But we need all shortest paths of the same length. Here's how we can adapt BFS:
-
Level-by-level exploration: Instead of stopping immediately when we find
endWord
, we continue processing all nodes at the current level. This ensures we find all paths of the same minimum length. -
Track predecessors, not just visited: In standard BFS, we mark nodes as visited to avoid revisiting them. However, since we need all paths, a word might be reached through multiple predecessors at the same level. For example, "dog" might be reached from both "dot" and "log" at the same distance from the start.
-
Build paths backwards: Rather than maintaining all partial paths during BFS (which would be memory-intensive), we can:
- Use BFS to find the shortest distance and record which words can lead to each word at each level
- Once we reach
endWord
, reconstruct all paths by backtracking through the predecessor relationships
-
Optimize word generation: For each word, we need to find all valid next words (those in
wordList
that differ by one letter). Instead of comparing with every word in the list, we can:- Try changing each character position to all 26 letters
- Check if the resulting word exists in our word set
- This is more efficient when the word list is large
The solution combines BFS for finding the shortest path length with a predecessor tracking mechanism that allows us to reconstruct all shortest paths through backtracking (DFS). The BFS phase builds a graph of predecessors, and the DFS phase extracts all valid paths from this graph.
Solution Approach
The implementation uses a two-phase approach: BFS to find shortest paths and build predecessor relationships, followed by DFS to reconstruct all paths.
Phase 1: BFS to Find Shortest Paths and Build Predecessor Graph
-
Initial Setup:
- Convert
wordList
to a setwords
for O(1) lookup - Check if
endWord
exists in the word list - if not, return empty result - Initialize
dist
dictionary to track the distance of each word frombeginWord
- Create
prev
as a defaultdict of sets to store predecessors for each word - Start BFS with
beginWord
in the queue
- Convert
-
Level-by-level BFS:
- Process all nodes at the current level before moving to the next (
step
tracks current distance) - For each word at current level:
- Generate all possible one-letter transformations by:
- Converting word to a list of characters
- For each position, try all 26 letters:
chr(ord('a') + j)
- Form new word and check validity
- Generate all possible one-letter transformations by:
- Process all nodes at the current level before moving to the next (
-
Predecessor Tracking:
- When we generate a transformed word
t
from wordp
:- If
dist.get(t, 0) == step
: Wordt
was already discovered at this level, addp
as another predecessor - If
t
is inwords
(not yet visited):- Add
p
as predecessor oft
- Remove
t
fromwords
to mark as visited - Add
t
to queue for next level - Record
dist[t] = step
- Add
- If
- When we generate a transformed word
-
Early Termination:
- Set
found = True
when we reachendWord
- Continue processing the current level to find all paths of same length
- Stop BFS after completing the level where
endWord
was found
- Set
Phase 2: DFS to Reconstruct All Paths
- Backtracking from End to Start:
- Start DFS from
endWord
with initial path[endWord]
- Recursively explore all predecessors:
- Base case: When we reach
beginWord
, we've found a complete path - Add the reversed path to results (since we built it backwards)
- Base case: When we reach
- Start DFS from
- Path Construction:
- For each word, iterate through all its predecessors
- Add predecessor to current path
- Recursively explore from that predecessor
- Remove predecessor from path (backtrack) to explore other options
Key Data Structures:
words
(set): O(1) lookup for valid wordsdist
(dict): Tracks shortest distance frombeginWord
to each wordprev
(defaultdict of sets): Maps each word to all its possible predecessors at the shortest distanceq
(deque): BFS queue for level-order traversalans
(list): Stores all shortest transformation sequences
The algorithm ensures we find all shortest paths by:
- Using BFS to guarantee shortest distance
- Tracking all predecessors at the same level
- Reconstructing all possible paths through backtracking
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to illustrate the solution approach.
Input:
beginWord = "red"
endWord = "tax"
wordList = ["ted", "tex", "rex", "tax"]
Phase 1: BFS to Find Shortest Paths and Build Predecessor Graph
Initial Setup:
- Convert wordList to set:
words = {"ted", "tex", "rex", "tax"}
- Check endWord "tax" exists ✓
- Initialize:
dist = {"red": 0}
,prev = {}
,q = ["red"]
Level 1 (step = 1):
- Process "red":
- Try all one-letter changes:
- Position 0: "aed", "bed", ..., "ted" ✓ (in words), ..., "zed"
- Position 1: "rad", "rbd", ..., "rex" ✓ (in words), ..., "rzd"
- Position 2: "rea", "reb", ..., "rez"
- Found valid words: "ted", "rex"
- Update:
prev["ted"] = {"red"}
,dist["ted"] = 1
prev["rex"] = {"red"}
,dist["rex"] = 1
- Remove "ted", "rex" from words
- Add both to queue
- Try all one-letter changes:
- After Level 1:
q = ["ted", "rex"]
,words = {"tex", "tax"}
Level 2 (step = 2):
-
Process "ted":
- Try all one-letter changes:
- Position 0: finds nothing new
- Position 1: "tad", "tbd", ..., "tex" ✓ (in words), ..., "tzd"
- Position 2: finds nothing new
- Found: "tex"
- Update:
prev["tex"] = {"ted"}
,dist["tex"] = 2
- Try all one-letter changes:
-
Process "rex":
- Try all one-letter changes:
- Position 0: "tex" (dist[tex] = 2 = step, so add as predecessor)
- Position 1: finds nothing new
- Position 2: finds nothing new
- Update:
prev["tex"] = {"ted", "rex"}
(rex added as another predecessor)
- Try all one-letter changes:
-
After Level 2:
q = ["tex"]
,words = {"tax"}
Level 3 (step = 3):
- Process "tex":
- Try all one-letter changes:
- Position 0: finds nothing new
- Position 1: "tax" ✓ (in words) - Found endWord!
- Position 2: finds nothing new
- Update:
prev["tax"] = {"tex"}
,dist["tax"] = 3
- Set
found = True
- Try all one-letter changes:
- Complete this level and stop BFS
Final predecessor graph:
prev = { "ted": {"red"}, "rex": {"red"}, "tex": {"ted", "rex"}, "tax": {"tex"} }
Phase 2: DFS to Reconstruct All Paths
Start DFS from "tax" with path ["tax"]:
-
Current word: "tax", path: ["tax"]
- Predecessors: {"tex"}
- Explore "tex"
-
Current word: "tex", path: ["tax", "tex"]
- Predecessors: {"ted", "rex"}
- First explore "ted"
-
Current word: "ted", path: ["tax", "tex", "ted"]
- Predecessors: {"red"}
- Explore "red"
-
Current word: "red", path: ["tax", "tex", "ted", "red"]
- This is beginWord! Add reversed path: ["red", "ted", "tex", "tax"]
- Backtrack to step 2
-
Back at "tex", now explore "rex"
- Current word: "rex", path: ["tax", "tex", "rex"]
- Predecessors: {"red"}
- Explore "red"
-
Current word: "red", path: ["tax", "tex", "rex", "red"]
- This is beginWord! Add reversed path: ["red", "rex", "tex", "tax"]
Final Result:
[ ["red", "ted", "tex", "tax"], ["red", "rex", "tex", "tax"] ]
Both paths have length 4, which is the shortest possible transformation sequence.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def findLadders(
6 self, beginWord: str, endWord: str, wordList: List[str]
7 ) -> List[List[str]]:
8 """
9 Find all shortest transformation sequences from beginWord to endWord.
10 Each transformation changes exactly one letter at a time.
11
12 Args:
13 beginWord: Starting word
14 endWord: Target word
15 wordList: List of valid intermediate words
16
17 Returns:
18 List of all shortest transformation sequences
19 """
20
21 def build_paths(current_path: List[str], current_word: str) -> None:
22 """
23 Recursively build all shortest paths from endWord back to beginWord.
24
25 Args:
26 current_path: Path being constructed (in reverse order)
27 current_word: Current word in the path construction
28 """
29 # Base case: reached the beginning word
30 if current_word == beginWord:
31 # Reverse the path to get correct order and add to results
32 result_paths.append(current_path[::-1])
33 return
34
35 # Explore all predecessor words that lead to current word
36 for predecessor_word in predecessors[current_word]:
37 current_path.append(predecessor_word)
38 build_paths(current_path, predecessor_word)
39 current_path.pop() # Backtrack
40
41 # Initialize result list
42 result_paths = []
43
44 # Convert wordList to set for O(1) lookup
45 available_words = set(wordList)
46
47 # Early termination if endWord is not in wordList
48 if endWord not in available_words:
49 return result_paths
50
51 # Remove beginWord from available words to avoid revisiting
52 available_words.discard(beginWord)
53
54 # Track distance from beginWord to each discovered word
55 word_distances = {beginWord: 0}
56
57 # Track all predecessors for each word (for path reconstruction)
58 predecessors = defaultdict(set)
59
60 # BFS queue initialization
61 bfs_queue = deque([beginWord])
62 target_found = False
63 current_distance = 0
64
65 # BFS to find all shortest paths
66 while bfs_queue and not target_found:
67 current_distance += 1
68
69 # Process all words at current distance level
70 level_size = len(bfs_queue)
71 for _ in range(level_size):
72 current_word = bfs_queue.popleft()
73 word_chars = list(current_word)
74
75 # Try changing each character position
76 for char_index in range(len(word_chars)):
77 original_char = word_chars[char_index]
78
79 # Try all 26 lowercase letters
80 for letter_code in range(26):
81 word_chars[char_index] = chr(ord('a') + letter_code)
82 transformed_word = ''.join(word_chars)
83
84 # If word was already discovered at same distance, add predecessor
85 if word_distances.get(transformed_word, 0) == current_distance:
86 predecessors[transformed_word].add(current_word)
87
88 # Skip if word is not valid or already processed
89 if transformed_word not in available_words:
90 continue
91
92 # Record predecessor and process new word
93 predecessors[transformed_word].add(current_word)
94 available_words.discard(transformed_word)
95 bfs_queue.append(transformed_word)
96 word_distances[transformed_word] = current_distance
97
98 # Check if target is reached
99 if transformed_word == endWord:
100 target_found = True
101
102 # Restore original character for next iteration
103 word_chars[char_index] = original_char
104
105 # Build all shortest paths if target was found
106 if target_found:
107 initial_path = [endWord]
108 build_paths(initial_path, endWord)
109
110 return result_paths
111
1class Solution {
2 // Store all shortest transformation sequences
3 private List<List<String>> result;
4 // Map to store predecessors for each word in the shortest paths
5 private Map<String, Set<String>> predecessors;
6
7 public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
8 result = new ArrayList<>();
9
10 // Convert word list to set for O(1) lookup
11 Set<String> wordSet = new HashSet<>(wordList);
12
13 // Early return if endWord is not in the word list
14 if (!wordSet.contains(endWord)) {
15 return result;
16 }
17
18 // Remove beginWord from set to avoid revisiting
19 wordSet.remove(beginWord);
20
21 // Track the distance (steps) from beginWord to each word
22 Map<String, Integer> distanceMap = new HashMap<>();
23 distanceMap.put(beginWord, 0);
24
25 // Initialize predecessors map
26 predecessors = new HashMap<>();
27
28 // BFS queue for level-order traversal
29 Queue<String> queue = new ArrayDeque<>();
30 queue.offer(beginWord);
31
32 boolean targetFound = false;
33 int currentStep = 0;
34
35 // BFS to find all shortest paths
36 while (!queue.isEmpty() && !targetFound) {
37 currentStep++;
38 int levelSize = queue.size();
39
40 // Process all words at current level
41 for (int i = 0; i < levelSize; i++) {
42 String currentWord = queue.poll();
43 char[] wordChars = currentWord.toCharArray();
44
45 // Try changing each character position
46 for (int charIndex = 0; charIndex < wordChars.length; charIndex++) {
47 char originalChar = wordChars[charIndex];
48
49 // Try all possible characters a-z
50 for (char newChar = 'a'; newChar <= 'z'; newChar++) {
51 wordChars[charIndex] = newChar;
52 String transformedWord = new String(wordChars);
53
54 // If we've seen this word at the same distance, add another predecessor
55 if (distanceMap.getOrDefault(transformedWord, 0) == currentStep) {
56 predecessors.get(transformedWord).add(currentWord);
57 }
58
59 // Skip if word not in wordSet (already visited or invalid)
60 if (!wordSet.contains(transformedWord)) {
61 continue;
62 }
63
64 // Record predecessor relationship
65 predecessors.computeIfAbsent(transformedWord, key -> new HashSet<>())
66 .add(currentWord);
67
68 // Mark as visited and add to queue
69 wordSet.remove(transformedWord);
70 queue.offer(transformedWord);
71 distanceMap.put(transformedWord, currentStep);
72
73 // Check if we've reached the target
74 if (endWord.equals(transformedWord)) {
75 targetFound = true;
76 }
77 }
78
79 // Restore original character
80 wordChars[charIndex] = originalChar;
81 }
82 }
83 }
84
85 // If target was found, reconstruct all shortest paths using DFS
86 if (targetFound) {
87 Deque<String> currentPath = new ArrayDeque<>();
88 currentPath.add(endWord);
89 buildPaths(currentPath, beginWord, endWord);
90 }
91
92 return result;
93 }
94
95 /**
96 * DFS to reconstruct all shortest paths from endWord to beginWord
97 * using the predecessors map
98 */
99 private void buildPaths(Deque<String> currentPath, String beginWord, String currentWord) {
100 // Base case: reached the beginning word
101 if (currentWord.equals(beginWord)) {
102 result.add(new ArrayList<>(currentPath));
103 return;
104 }
105
106 // Recursively build paths through all predecessors
107 for (String predecessor : predecessors.get(currentWord)) {
108 currentPath.addFirst(predecessor);
109 buildPaths(currentPath, beginWord, predecessor);
110 currentPath.removeFirst(); // Backtrack
111 }
112 }
113}
114
1class Solution {
2private:
3 // Store all shortest transformation sequences
4 vector<vector<string>> result;
5 // Map to store predecessors for each word in the shortest paths
6 unordered_map<string, unordered_set<string>> predecessors;
7
8public:
9 vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
10 // Convert word list to set for O(1) lookup
11 unordered_set<string> wordSet(wordList.begin(), wordList.end());
12
13 // Early return if endWord is not in the word list
14 if (wordSet.find(endWord) == wordSet.end()) {
15 return result;
16 }
17
18 // Remove beginWord from set to avoid revisiting
19 wordSet.erase(beginWord);
20
21 // Track the distance (steps) from beginWord to each word
22 unordered_map<string, int> distanceMap;
23 distanceMap[beginWord] = 0;
24
25 // BFS queue for level-order traversal
26 queue<string> bfsQueue;
27 bfsQueue.push(beginWord);
28
29 bool targetFound = false;
30 int currentStep = 0;
31
32 // BFS to find all shortest paths
33 while (!bfsQueue.empty() && !targetFound) {
34 currentStep++;
35 int levelSize = bfsQueue.size();
36
37 // Process all words at current level
38 for (int i = 0; i < levelSize; i++) {
39 string currentWord = bfsQueue.front();
40 bfsQueue.pop();
41
42 // Try changing each character position
43 for (int charIndex = 0; charIndex < currentWord.length(); charIndex++) {
44 char originalChar = currentWord[charIndex];
45
46 // Try all possible characters a-z
47 for (char newChar = 'a'; newChar <= 'z'; newChar++) {
48 currentWord[charIndex] = newChar;
49 string transformedWord = currentWord;
50
51 // If we've seen this word at the same distance, add another predecessor
52 if (distanceMap.count(transformedWord) && distanceMap[transformedWord] == currentStep) {
53 predecessors[transformedWord].insert(currentWord);
54 }
55
56 // Skip if word not in wordSet (already visited or invalid)
57 if (wordSet.find(transformedWord) == wordSet.end()) {
58 continue;
59 }
60
61 // Record predecessor relationship
62 predecessors[transformedWord].insert(currentWord);
63
64 // Mark as visited and add to queue
65 wordSet.erase(transformedWord);
66 bfsQueue.push(transformedWord);
67 distanceMap[transformedWord] = currentStep;
68
69 // Check if we've reached the target
70 if (endWord == transformedWord) {
71 targetFound = true;
72 }
73 }
74
75 // Restore original character
76 currentWord[charIndex] = originalChar;
77 }
78 }
79 }
80
81 // If target was found, reconstruct all shortest paths using DFS
82 if (targetFound) {
83 vector<string> currentPath;
84 currentPath.push_back(endWord);
85 buildPaths(currentPath, beginWord, endWord);
86 }
87
88 return result;
89 }
90
91private:
92 /**
93 * DFS to reconstruct all shortest paths from endWord to beginWord
94 * using the predecessors map
95 */
96 void buildPaths(vector<string>& currentPath, const string& beginWord, const string& currentWord) {
97 // Base case: reached the beginning word
98 if (currentWord == beginWord) {
99 // Create a copy of the path in reverse order
100 vector<string> completePath(currentPath.rbegin(), currentPath.rend());
101 result.push_back(completePath);
102 return;
103 }
104
105 // Recursively build paths through all predecessors
106 for (const string& predecessor : predecessors[currentWord]) {
107 currentPath.push_back(predecessor);
108 buildPaths(currentPath, beginWord, predecessor);
109 currentPath.pop_back(); // Backtrack
110 }
111 }
112};
113
1// Store all shortest transformation sequences
2let result: string[][];
3// Map to store predecessors for each word in the shortest paths
4let predecessors: Map<string, Set<string>>;
5
6function findLadders(beginWord: string, endWord: string, wordList: string[]): string[][] {
7 result = [];
8
9 // Convert word list to set for O(1) lookup
10 const wordSet = new Set<string>(wordList);
11
12 // Early return if endWord is not in the word list
13 if (!wordSet.has(endWord)) {
14 return result;
15 }
16
17 // Remove beginWord from set to avoid revisiting
18 wordSet.delete(beginWord);
19
20 // Track the distance (steps) from beginWord to each word
21 const distanceMap = new Map<string, number>();
22 distanceMap.set(beginWord, 0);
23
24 // Initialize predecessors map
25 predecessors = new Map<string, Set<string>>();
26
27 // BFS queue for level-order traversal
28 const queue: string[] = [];
29 queue.push(beginWord);
30
31 let targetFound = false;
32 let currentStep = 0;
33
34 // BFS to find all shortest paths
35 while (queue.length > 0 && !targetFound) {
36 currentStep++;
37 const levelSize = queue.length;
38
39 // Process all words at current level
40 for (let i = 0; i < levelSize; i++) {
41 const currentWord = queue.shift()!;
42 const wordChars = currentWord.split('');
43
44 // Try changing each character position
45 for (let charIndex = 0; charIndex < wordChars.length; charIndex++) {
46 const originalChar = wordChars[charIndex];
47
48 // Try all possible characters a-z
49 for (let charCode = 97; charCode <= 122; charCode++) {
50 const newChar = String.fromCharCode(charCode);
51 wordChars[charIndex] = newChar;
52 const transformedWord = wordChars.join('');
53
54 // If we've seen this word at the same distance, add another predecessor
55 if ((distanceMap.get(transformedWord) ?? 0) === currentStep) {
56 predecessors.get(transformedWord)?.add(currentWord);
57 }
58
59 // Skip if word not in wordSet (already visited or invalid)
60 if (!wordSet.has(transformedWord)) {
61 continue;
62 }
63
64 // Record predecessor relationship
65 if (!predecessors.has(transformedWord)) {
66 predecessors.set(transformedWord, new Set<string>());
67 }
68 predecessors.get(transformedWord)!.add(currentWord);
69
70 // Mark as visited and add to queue
71 wordSet.delete(transformedWord);
72 queue.push(transformedWord);
73 distanceMap.set(transformedWord, currentStep);
74
75 // Check if we've reached the target
76 if (endWord === transformedWord) {
77 targetFound = true;
78 }
79 }
80
81 // Restore original character
82 wordChars[charIndex] = originalChar;
83 }
84 }
85 }
86
87 // If target was found, reconstruct all shortest paths using DFS
88 if (targetFound) {
89 const currentPath: string[] = [endWord];
90 buildPaths(currentPath, beginWord, endWord);
91 }
92
93 return result;
94}
95
96/**
97 * DFS to reconstruct all shortest paths from endWord to beginWord
98 * using the predecessors map
99 */
100function buildPaths(currentPath: string[], beginWord: string, currentWord: string): void {
101 // Base case: reached the beginning word
102 if (currentWord === beginWord) {
103 result.push([...currentPath]);
104 return;
105 }
106
107 // Recursively build paths through all predecessors
108 const currentPredecessors = predecessors.get(currentWord);
109 if (currentPredecessors) {
110 for (const predecessor of currentPredecessors) {
111 currentPath.unshift(predecessor);
112 buildPaths(currentPath, beginWord, predecessor);
113 currentPath.shift(); // Backtrack
114 }
115 }
116}
117
Time and Space Complexity
Time Complexity: O(N * M * 26 + N * M * P)
where:
N
is the number of words in the wordListM
is the length of each wordP
is the number of shortest paths from beginWord to endWord
The BFS portion takes O(N * M * 26)
:
- We potentially visit all
N
words in the worst case - For each word, we iterate through all
M
characters - For each character position, we try all 26 lowercase letters
- String operations like creating the new word take
O(M)
time
The DFS reconstruction takes O(P * L)
where L
is the length of the shortest path:
- We traverse each of the
P
paths - Each path has length
L
(the shortest distance) - In the worst case,
P
could be exponential, but typicallyP * L ≈ N * M
in practice
Space Complexity: O(N * M + N^2)
where:
words
set stores up toN
words, each of lengthM
:O(N * M)
dist
dictionary stores up toN
entries:O(N)
prev
dictionary can store up toN
entries, and each entry can have up toN
predecessors in the worst case:O(N^2)
- BFS queue can contain up to
N
words:O(N * M)
- DFS recursion stack depth is at most the length of the shortest path:
O(M)
in the worst case ans
stores all shortest paths, which could beO(P * L * M)
whereP
is the number of paths andL
is the path length
The dominant space factor is typically O(N * M)
for storing the words and O(N^2)
for the predecessor relationships in dense transformation graphs.
Common Pitfalls
1. Not Handling Multiple Predecessors at the Same Level
One of the most common mistakes is failing to recognize that a word can be reached from multiple predecessors at the same distance level. This happens when different words at level n
can transform into the same word at level n+1
.
Incorrect Approach:
# Wrong: Only storing one predecessor per word predecessors = {} # dict instead of defaultdict(set) if transformed_word in available_words: predecessors[transformed_word] = current_word # Overwrites previous predecessor!
Why it fails: This would only find one path even when multiple shortest paths exist. For example, if both "hot" and "got" can transform to "pot" at the same level, only one predecessor would be recorded.
Correct Solution:
# Use defaultdict(set) to store ALL predecessors at the same level
predecessors = defaultdict(set)
if word_distances.get(transformed_word, 0) == current_distance:
predecessors[transformed_word].add(current_word)
2. Revisiting Words Already Processed at Current Level
Another critical mistake is not properly handling words that are discovered multiple times within the same BFS level. We need to add all valid predecessors but avoid adding the word to the queue multiple times.
Incorrect Approach:
# Wrong: Adding word to queue every time it's discovered for predecessor in current_level: for transformed_word in get_transformations(predecessor): if transformed_word in available_words: bfs_queue.append(transformed_word) # Duplicate additions! predecessors[transformed_word].add(predecessor)
Why it fails: This leads to duplicate processing and incorrect distance calculations. A word might be added to the queue multiple times within the same level, causing it to be processed redundantly.
Correct Solution:
# Check if word was already discovered at this level if word_distances.get(transformed_word, 0) == current_distance: # Already discovered at this level - just add predecessor predecessors[transformed_word].add(current_word) elif transformed_word in available_words: # First time discovering - add to queue and mark as visited predecessors[transformed_word].add(current_word) available_words.discard(transformed_word) # Remove to prevent re-adding bfs_queue.append(transformed_word) word_distances[transformed_word] = current_distance
3. Incorrect Level-by-Level BFS Processing
A subtle but important mistake is not processing BFS strictly level by level, which can lead to finding paths that aren't actually the shortest.
Incorrect Approach:
# Wrong: Not tracking level boundaries properly while bfs_queue: current_word = bfs_queue.popleft() if current_word == endWord: break # Might miss other paths at the same level! # ... generate transformations
Why it fails: If you break immediately upon finding the target, you miss other words at the same level that could also lead to the target, resulting in incomplete results.
Correct Solution:
# Process entire level before moving to next
while bfs_queue and not target_found:
current_distance += 1
level_size = len(bfs_queue) # Capture current level size
for _ in range(level_size): # Process exactly this many words
current_word = bfs_queue.popleft()
# ... process transformations
if transformed_word == endWord:
target_found = True # Mark found but continue processing level
# Only stop after completing the level where target was found
These pitfalls often result in either missing valid shortest paths or including paths that aren't actually the shortest. The key is maintaining proper level boundaries in BFS and correctly tracking all predecessors at each level.
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!