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 wordendWord
: The target word we want to reachwordList
: A list of valid words that can be used as intermediate steps
Rules for transformation:
- You can only change one letter at a time to form the next word
- Each intermediate word must be present in
wordList
(exceptbeginWord
which doesn't need to be in the list) - 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:
- Starts from
beginWord
and explores all valid one-letter transformations - For each word, tries changing each character to all 26 possible letters
- Checks if the new word exists in the word list
- Continues until
endWord
is found or all possibilities are exhausted - 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
toendWord
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
toendWord
.
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.
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 fromwordList
for O(1) lookupq
: A deque (double-ended queue) for BFS traversalans
: Counter to track the number of words in the transformation sequence
Algorithm Steps:
-
Initialize the BFS:
- Convert
wordList
to a set for fast membership checking - Add
beginWord
to the queue - Set
ans = 1
(countingbeginWord
)
- Convert
-
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))
- For each level, increment
-
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
-
Process valid neighbors:
- If the new word equals
endWord
, we've found the shortest path - returnans
- Otherwise, add the new word to the queue for the next level
- Remove the word from the set to mark it as visited
- If the new word equals
-
Handle no solution:
- If the queue becomes empty without finding
endWord
, return 0
- If the queue becomes empty without finding
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) andq2
(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 EvaluatorExample 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:
- BFS explores level by level, ensuring the first path found is the shortest
- Each word is visited only once (removed from set after visiting)
- We generate neighbors dynamically by trying all 26 letters at each position
- 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)
- Converting list to string with
- 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 lengthM
- 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
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!