Facebook Pixel

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 word
  • endWord: The target word
  • wordList: 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:

  1. 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.

  2. Valid Words Only: Every intermediate word in the transformation must exist in wordList. The beginWord doesn't need to be in the list, but all other words (including endWord) must be present.

  3. 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", and wordList = ["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 to endWord.

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 to endWord. 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:

  1. It finds the shortest path in unweighted graphs
  2. It can explore all nodes at the same distance simultaneously
  3. We can track multiple paths of the same minimum length by recording predecessors at each level
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. 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.

  3. 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
  4. 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

  1. Initial Setup:

    • Convert wordList to a set words 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 from beginWord
    • Create prev as a defaultdict of sets to store predecessors for each word
    • Start BFS with beginWord in the queue
  2. 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
  3. Predecessor Tracking:

    • When we generate a transformed word t from word p:
      • If dist.get(t, 0) == step: Word t was already discovered at this level, add p as another predecessor
      • If t is in words (not yet visited):
        • Add p as predecessor of t
        • Remove t from words to mark as visited
        • Add t to queue for next level
        • Record dist[t] = step
  4. Early Termination:

    • Set found = True when we reach endWord
    • Continue processing the current level to find all paths of same length
    • Stop BFS after completing the level where endWord was found

Phase 2: DFS to Reconstruct All Paths

  1. 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)
  2. 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 words
  • dist (dict): Tracks shortest distance from beginWord to each word
  • prev (defaultdict of sets): Maps each word to all its possible predecessors at the shortest distance
  • q (deque): BFS queue for level-order traversal
  • ans (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 Evaluator

Example 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
  • 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
  • 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)
  • 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
  • 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"]:

  1. Current word: "tax", path: ["tax"]

    • Predecessors: {"tex"}
    • Explore "tex"
  2. Current word: "tex", path: ["tax", "tex"]

    • Predecessors: {"ted", "rex"}
    • First explore "ted"
  3. Current word: "ted", path: ["tax", "tex", "ted"]

    • Predecessors: {"red"}
    • Explore "red"
  4. Current word: "red", path: ["tax", "tex", "ted", "red"]

    • This is beginWord! Add reversed path: ["red", "ted", "tex", "tax"]
    • Backtrack to step 2
  5. Back at "tex", now explore "rex"

    • Current word: "rex", path: ["tax", "tex", "rex"]
    • Predecessors: {"red"}
    • Explore "red"
  6. 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 wordList
  • M is the length of each word
  • P 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 typically P * L ≈ N * M in practice

Space Complexity: O(N * M + N^2) where:

  • words set stores up to N words, each of length M: O(N * M)
  • dist dictionary stores up to N entries: O(N)
  • prev dictionary can store up to N entries, and each entry can have up to N 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 be O(P * L * M) where P is the number of paths and L 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More