Facebook Pixel

127. Word Ladder

Problem Description

This problem asks you to find the shortest transformation sequence between two words by changing one letter at a time, where each intermediate word must exist in a given dictionary.

Given:

  • beginWord: The starting word
  • endWord: The target word we want to reach
  • wordList: A list of valid words that can be used as intermediate steps

Rules for transformation:

  1. You can only change one letter at a time to form the next word
  2. Each intermediate word must be present in wordList (except beginWord which doesn't need to be in the list)
  3. The final word in the sequence must be endWord

The task is to return the number of words in the shortest transformation sequence from beginWord to endWord. If no valid transformation sequence exists, return 0.

For example, if beginWord = "hit", endWord = "cog", and wordList = ["hot","dot","dog","lot","log","cog"], one possible shortest sequence would be: "hit" -> "hot" -> "dot" -> "dog" -> "cog"

This sequence has 5 words total, so the answer would be 5.

The solution uses Breadth-First Search (BFS) to find the shortest path. It explores all possible one-letter transformations level by level, ensuring that when the target word is found, it's reached via the shortest possible path. The algorithm:

  1. Starts from beginWord and explores all valid one-letter transformations
  2. For each word, tries changing each character to all 26 possible letters
  3. Checks if the new word exists in the word list
  4. Continues until endWord is found or all possibilities are exhausted
  5. Removes visited words from the set to avoid revisiting them

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 the shortest path from beginWord to endWord through this graph.

Is it a tree?

  • No: The word transformation graph is not a tree. It can have cycles (you could transform word A to B, then B to C, and potentially C back to A if they all differ by one letter), and nodes can have multiple parents.

Is the problem related to directed acyclic graphs?

  • No: The graph is undirected (transformations work both ways) and can contain cycles, so it's not a DAG.

Is the problem related to shortest paths?

  • Yes: We need to find the shortest transformation sequence (minimum number of steps) from beginWord to endWord.

Is the graph Weighted?

  • No: Each transformation (edge) has the same cost of 1. We're counting the number of transformations, not dealing with different weights/costs for different transformations.

BFS

  • Yes: Since we have an unweighted graph and need the shortest path, BFS is the perfect algorithm.

Conclusion: The flowchart leads us to use BFS (Breadth-First Search) for finding the shortest transformation sequence in this unweighted graph problem. BFS explores all words at distance k before exploring words at distance k+1, guaranteeing that when we reach endWord, we've found the shortest path.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that this is a shortest path problem in an unweighted graph. Each word represents a node, and two words are connected if they differ by exactly one letter. Since we want the minimum number of transformations, we need to find the shortest path from beginWord to endWord.

Why BFS? When all edges have equal weight (each transformation counts as 1 step), BFS naturally finds the shortest path. It explores all words reachable in 1 step, then all words reachable in 2 steps, and so on. This level-by-level exploration guarantees that the first time we reach endWord, we've found the shortest path.

The clever part of the implementation is how we find neighbors. Instead of comparing every word with every other word to build the graph (which would be expensive), we generate all possible one-letter transformations on the fly. For each position in the current word, we try all 26 letters and check if the resulting word exists in our word list.

To avoid revisiting words and getting stuck in cycles, we remove words from the set once visited. This serves as our "visited" marker and prevents infinite loops. Think of it like marking rooms as explored in a maze - once you've been to a room at distance d, there's no point visiting it again at distance d+1 or greater.

The algorithm maintains a counter for the number of words in the path. We start with 1 (for beginWord) and increment it each time we move to the next level of BFS. When we find endWord, we return this counter. If the queue becomes empty without finding endWord, it means no valid transformation sequence exists, so we return 0.

Solution Approach

The solution implements a standard BFS approach with some optimizations for finding neighbors efficiently.

Data Structures Used:

  • words: A set containing all valid words from wordList for O(1) lookup
  • q: A deque (double-ended queue) for BFS traversal
  • ans: Counter to track the number of words in the transformation sequence

Algorithm Steps:

  1. Initialize the BFS:

    • Convert wordList to a set for fast membership checking
    • Add beginWord to the queue
    • Set ans = 1 (counting beginWord)
  2. Level-by-level BFS traversal:

    • For each level, increment ans at the start (representing the next step)
    • Process all words at the current level using for _ in range(len(q))
  3. Generate neighbors for each word:

    • Pop a word from the queue and convert it to a list of characters
    • For each position i in the word:
      • Save the original character
      • Try all 26 possible letters ('a' to 'z')
      • Form the new word by joining the modified character list
      • Check if this new word exists in our word set
  4. Process valid neighbors:

    • If the new word equals endWord, we've found the shortest path - return ans
    • Otherwise, add the new word to the queue for the next level
    • Remove the word from the set to mark it as visited
  5. Handle no solution:

    • If the queue becomes empty without finding endWord, return 0

Key Optimization: The reference mentions Bidirectional BFS as an advanced optimization. Instead of searching only from start to end, we search from both directions simultaneously:

  • Maintain two queues: q1 (start β†’ end) and q2 (end β†’ start)
  • Always expand the smaller queue to reduce search space
  • When a word is found in both directions, we've found the shortest path
  • The total distance is step1 + step2 + 1

This bidirectional approach significantly reduces the search space, especially when the shortest path is long, as we explore from both ends rather than expanding exponentially from just one direction.

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 walk through a small example to illustrate the BFS solution approach.

Input:

  • beginWord = "hit"
  • endWord = "cog"
  • wordList = ["hot", "dot", "dog", "cog"]

Step-by-step execution:

Initialization:

  • Convert wordList to set: words = {"hot", "dot", "dog", "cog"}
  • Queue: q = ["hit"]
  • Answer counter: ans = 1 (counting "hit")

Level 1 (ans = 2):

  • Process "hit" (only word in queue)
  • Generate all one-letter transformations:
    • Position 0: "ait", "bit", ..., "zit" β†’ only "hot" is in words set
    • Position 1: "hat", "hbt", ..., "hzt" β†’ none in words set
    • Position 2: "hia", "hib", ..., "hiz" β†’ none in words set
  • Found neighbor: "hot"
  • Add "hot" to queue, remove from set
  • Queue: q = ["hot"], words: {"dot", "dog", "cog"}

Level 2 (ans = 3):

  • Process "hot"
  • Generate transformations:
    • Position 0: "aot", "bot", ..., "dot", ..., "zot" β†’ "dot" is in words set
    • Position 1: "hat", "hbt", ..., "hzt" β†’ none in words set
    • Position 2: "hoa", "hob", ..., "hoz" β†’ none in words set
  • Found neighbor: "dot"
  • Queue: q = ["dot"], words: {"dog", "cog"}

Level 3 (ans = 4):

  • Process "dot"
  • Generate transformations:
    • Position 0: "aot", "bot", ..., "zot" β†’ none in remaining words
    • Position 1: "dat", "dbt", ..., "dzt" β†’ none in remaining words
    • Position 2: "doa", "dob", ..., "dog", ..., "doz" β†’ "dog" is in words set
  • Found neighbor: "dog"
  • Queue: q = ["dog"], words: {"cog"}

Level 4 (ans = 5):

  • Process "dog"
  • Generate transformations:
    • Position 0: "aog", "bog", "cog", ..., "zog" β†’ "cog" is in words set!
  • Found "cog" which equals endWord
  • Return ans = 5

The transformation sequence is: "hit" β†’ "hot" β†’ "dot" β†’ "dog" β†’ "cog" (5 words total)

Key observations:

  1. BFS explores level by level, ensuring the first path found is the shortest
  2. Each word is visited only once (removed from set after visiting)
  3. We generate neighbors dynamically by trying all 26 letters at each position
  4. The answer increments at each level, counting the number of words in the path

Solution Implementation

1from collections import deque
2from typing import List
3
4class Solution:
5    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
6        # Convert word list to set for O(1) lookup
7        available_words = set(wordList)
8      
9        # Early termination if endWord is not in wordList
10        if endWord not in available_words:
11            return 0
12      
13        # Initialize BFS queue with the starting word
14        queue = deque([beginWord])
15      
16        # Track the transformation sequence length (starting at 1 for beginWord)
17        transformation_length = 1
18      
19        # BFS to find shortest transformation sequence
20        while queue:
21            transformation_length += 1
22          
23            # Process all words at current level
24            current_level_size = len(queue)
25            for _ in range(current_level_size):
26                current_word = queue.popleft()
27              
28                # Convert string to list for character manipulation
29                word_chars = list(current_word)
30              
31                # Try changing each character position
32                for position in range(len(word_chars)):
33                    # Store original character to restore later
34                    original_char = word_chars[position]
35                  
36                    # Try all 26 lowercase letters
37                    for letter_index in range(26):
38                        word_chars[position] = chr(ord('a') + letter_index)
39                      
40                        # Form the new word
41                        new_word = ''.join(word_chars)
42                      
43                        # Skip if the new word is not in the available words
44                        if new_word not in available_words:
45                            continue
46                      
47                        # Check if we've reached the target word
48                        if new_word == endWord:
49                            return transformation_length
50                      
51                        # Add valid transformation to queue for next level
52                        queue.append(new_word)
53                      
54                        # Remove word from available set to avoid revisiting
55                        available_words.remove(new_word)
56                  
57                    # Restore the original character for next position
58                    word_chars[position] = original_char
59      
60        # No transformation sequence found
61        return 0
62
1class Solution {
2    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
3        // Convert word list to HashSet for O(1) lookup
4        Set<String> wordSet = new HashSet<>(wordList);
5      
6        // Initialize BFS queue with the starting word
7        Queue<String> queue = new ArrayDeque<>();
8        queue.offer(beginWord);
9      
10        // Track the transformation sequence length (starts at 1 for beginWord)
11        int transformationSteps = 1;
12      
13        // BFS traversal
14        while (!queue.isEmpty()) {
15            // Increment steps for the next level of transformations
16            transformationSteps++;
17          
18            // Process all words at the current level
19            int currentLevelSize = queue.size();
20            for (int i = 0; i < currentLevelSize; i++) {
21                String currentWord = queue.poll();
22                char[] charArray = currentWord.toCharArray();
23              
24                // Try changing each character position
25                for (int charIndex = 0; charIndex < charArray.length; charIndex++) {
26                    // Store original character to restore later
27                    char originalChar = charArray[charIndex];
28                  
29                    // Try all possible lowercase letters
30                    for (char newChar = 'a'; newChar <= 'z'; newChar++) {
31                        charArray[charIndex] = newChar;
32                        String transformedWord = new String(charArray);
33                      
34                        // Skip if the transformed word is not in the word list
35                        if (!wordSet.contains(transformedWord)) {
36                            continue;
37                        }
38                      
39                        // Check if we've reached the target word
40                        if (endWord.equals(transformedWord)) {
41                            return transformationSteps;
42                        }
43                      
44                        // Add valid transformation to queue for next level
45                        queue.offer(transformedWord);
46                      
47                        // Remove from set to avoid revisiting (mark as visited)
48                        wordSet.remove(transformedWord);
49                    }
50                  
51                    // Restore the original character for next position
52                    charArray[charIndex] = originalChar;
53                }
54            }
55        }
56      
57        // No transformation sequence found
58        return 0;
59    }
60}
61
1class Solution {
2public:
3    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
4        // Convert word list to unordered set for O(1) lookup
5        unordered_set<string> availableWords(wordList.begin(), wordList.end());
6      
7        // Initialize BFS queue with the starting word
8        queue<string> bfsQueue;
9        bfsQueue.push(beginWord);
10      
11        // Track the transformation sequence length (starting from 1)
12        int sequenceLength = 1;
13      
14        // Perform level-order BFS traversal
15        while (!bfsQueue.empty()) {
16            // Increment length for each level of BFS
17            ++sequenceLength;
18          
19            // Process all words at the current level
20            int currentLevelSize = bfsQueue.size();
21            for (int i = 0; i < currentLevelSize; ++i) {
22                // Get and remove the front word from queue
23                string currentWord = bfsQueue.front();
24                bfsQueue.pop();
25              
26                // Try changing each character position
27                for (int charIndex = 0; charIndex < currentWord.size(); ++charIndex) {
28                    // Store original character for restoration
29                    char originalChar = currentWord[charIndex];
30                  
31                    // Try all possible lowercase letters
32                    for (char newChar = 'a'; newChar <= 'z'; ++newChar) {
33                        // Replace character at current position
34                        currentWord[charIndex] = newChar;
35                      
36                        // Skip if the transformed word is not in the available word set
37                        if (!availableWords.count(currentWord)) {
38                            continue;
39                        }
40                      
41                        // Check if we've reached the target word
42                        if (currentWord == endWord) {
43                            return sequenceLength;
44                        }
45                      
46                        // Add valid transformation to queue for next level
47                        bfsQueue.push(currentWord);
48                      
49                        // Remove word from available set to avoid revisiting
50                        availableWords.erase(currentWord);
51                    }
52                  
53                    // Restore the original character for next iteration
54                    currentWord[charIndex] = originalChar;
55                }
56            }
57        }
58      
59        // No transformation sequence found
60        return 0;
61    }
62};
63
1/**
2 * Finds the minimum transformation sequence length from beginWord to endWord
3 * using words from wordList, changing one letter at a time
4 * @param beginWord - Starting word
5 * @param endWord - Target word to transform to
6 * @param wordList - List of valid intermediate words
7 * @returns Minimum number of transformations needed, or 0 if impossible
8 */
9function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
10    // Check if the target word exists in the word list
11    if (!wordList.includes(endWord)) {
12        return 0;
13    }
14
15    /**
16     * Helper function to replace a character at a specific index
17     * @param str - Original string
18     * @param index - Position to replace
19     * @param char - New character to insert
20     * @returns Modified string
21     */
22    const replaceCharAt = (str: string, index: number, char: string): string => {
23        return str.slice(0, index) + char + str.slice(index + 1);
24    };
25
26    const wordLength = beginWord.length;
27  
28    // Map each word to its pattern variations (with wildcards)
29    const wordToPatterns: Record<string, string[]> = {};
30  
31    // Map each pattern to words that match it
32    const patternToWords: Record<string, string[]> = {};
33
34    // Build the graph structure for all words including beginWord
35    const allWords = [beginWord, ...wordList];
36  
37    for (const word of allWords) {
38        const patterns: string[] = [];
39
40        // Generate all possible patterns by replacing each character with '*'
41        for (let i = 0; i < wordLength; i++) {
42            const pattern = replaceCharAt(word, i, '*');
43            patterns.push(pattern);
44
45            // Initialize pattern group if not exists
46            patternToWords[pattern] = patternToWords[pattern] || [];
47            patternToWords[pattern].push(word);
48        }
49
50        wordToPatterns[word] = patterns;
51    }
52
53    // BFS initialization
54    let transformationCount = 0;
55    let currentLevelPatterns = wordToPatterns[beginWord];
56    const visitedWords = new Set<string>([beginWord]);
57
58    // Perform BFS to find shortest transformation path
59    while (currentLevelPatterns.length > 0) {
60        const nextLevelPatterns: string[] = [];
61        transformationCount++;
62
63        // Process all patterns at current BFS level
64        for (const pattern of currentLevelPatterns) {
65            // Check all words matching this pattern
66            for (const word of patternToWords[pattern]) {
67                // Found the target word
68                if (word === endWord) {
69                    return transformationCount + 1;
70                }
71
72                // Skip already visited words
73                if (visitedWords.has(word)) {
74                    continue;
75                }
76              
77                // Mark word as visited
78                visitedWords.add(word);
79
80                // Add all patterns of this word to next level
81                nextLevelPatterns.push(...wordToPatterns[word]);
82            }
83        }
84
85        currentLevelPatterns = nextLevelPatterns;
86    }
87
88    // No transformation sequence found
89    return 0;
90}
91

Time and Space Complexity

Time Complexity: O(MΒ² Γ— N), where M is the length of each word and N is the total number of words in the word list.

  • The algorithm uses BFS to traverse through possible word transformations
  • In the worst case, we visit every word in the wordList: O(N)
  • For each word visited, we iterate through all character positions: O(M)
  • For each position, we try all 26 lowercase letters: O(26) = O(1)
  • For each generated word, we perform string operations:
    • Converting list to string with ''.join(s): O(M)
    • Checking membership in the set t not in words: O(M) for string hashing
    • Comparing strings t == endWord: O(M)
  • Total: O(N Γ— M Γ— 26 Γ— M) = O(MΒ² Γ— N)

Space Complexity: O(M Γ— N)

  • The words set stores all words from wordList: O(M Γ— N) where each word has length M
  • The BFS queue can contain at most N words in the worst case: O(M Γ— N)
  • The temporary list s for character manipulation: O(M)
  • Overall space complexity: O(M Γ— N)

Common Pitfalls

1. Forgetting to Check if endWord Exists in wordList

One of the most common mistakes is not verifying whether endWord is present in the word list before starting the BFS. If endWord isn't in the list, it's impossible to reach it, yet the algorithm might waste time exploring all possibilities.

Problem Code:

# Missing this crucial check
available_words = set(wordList)
queue = deque([beginWord])
# Directly starts BFS without checking endWord

Solution:

available_words = set(wordList)
# Early termination check
if endWord not in available_words:
    return 0
queue = deque([beginWord])

2. Not Processing Words Level by Level

A critical mistake is processing words continuously without maintaining level boundaries. This breaks the BFS property of finding the shortest path and makes it impossible to track the correct transformation length.

Problem Code:

while queue:
    current_word = queue.popleft()
    # Incorrectly increments for each word, not each level
    transformation_length += 1
    # ... generate neighbors

Solution:

while queue:
    transformation_length += 1
    # Process all words at the current level together
    current_level_size = len(queue)
    for _ in range(current_level_size):
        current_word = queue.popleft()
        # ... generate neighbors

3. String Immutability Issues

Attempting to modify strings directly (which are immutable in Python) or inefficiently creating new strings for each character change.

Problem Code:

for position in range(len(current_word)):
    for letter in 'abcdefghijklmnopqrstuvwxyz':
        # Inefficient string concatenation
        new_word = current_word[:position] + letter + current_word[position+1:]

Solution:

# Convert to list for efficient character manipulation
word_chars = list(current_word)
for position in range(len(word_chars)):
    original_char = word_chars[position]
    for letter_index in range(26):
        word_chars[position] = chr(ord('a') + letter_index)
        new_word = ''.join(word_chars)
    word_chars[position] = original_char  # Restore for next position

4. Not Removing Visited Words from the Set

Failing to mark words as visited leads to infinite loops or redundant processing. Words might be added to the queue multiple times, causing incorrect distance calculations and performance issues.

Problem Code:

if new_word in available_words:
    if new_word == endWord:
        return transformation_length
    queue.append(new_word)
    # Forgot to remove the word from available_words

Solution:

if new_word in available_words:
    if new_word == endWord:
        return transformation_length
    queue.append(new_word)
    # Mark as visited by removing from set
    available_words.remove(new_word)

5. Including beginWord in the Available Words Set

If beginWord happens to be in wordList, not handling it properly can cause the algorithm to revisit it, creating cycles or incorrect paths.

Problem Code:

available_words = set(wordList)
# If beginWord is in wordList, it might be revisited
queue = deque([beginWord])

Solution:

available_words = set(wordList)
# Remove beginWord if it exists to prevent revisiting
available_words.discard(beginWord)  # discard() won't raise error if not present
queue = deque([beginWord])

6. Incorrect Initial Counter Value

Starting with transformation_length = 0 or incrementing at the wrong time leads to off-by-one errors in the final count.

Problem Code:

transformation_length = 0  # Wrong: should start at 1 for beginWord
while queue:
    # Or incrementing after processing instead of before
    for _ in range(len(queue)):
        # process words
    transformation_length += 1  # Wrong timing

Solution:

# Start at 1 to count beginWord
transformation_length = 1
while queue:
    # Increment at the start of each new level
    transformation_length += 1
    current_level_size = len(queue)
    for _ in range(current_level_size):
        # process words
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

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

Load More