Word Ladder II

HARD

Description

A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that:

  • Every adjacent pair of words differs by a single letter.
  • Every si for 1 <= i <= k is in wordList. Note that beginWord does not need to be in wordList.
  • sk == endWord

Given two words, beginWord and endWord, and a dictionary wordList, return all the shortest transformation sequences from beginWord to endWord, or an empty list if no such sequence exists. Each sequence should be returned as a list of the words [beginWord, s1, s2, ..., sk].

 

Example 1:

Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
Output: [["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]
Explanation: There are 2 shortest transformation sequences:
"hit" -> "hot" -> "dot" -> "dog" -> "cog"
"hit" -> "hot" -> "lot" -> "log" -> "cog"

Example 2:

Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
Output: []
Explanation: The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.

 

Constraints:

  • 1 <= beginWord.length <= 5
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 500
  • wordList[i].length == beginWord.length
  • beginWord, endWord, and wordList[i] consist of lowercase English letters.
  • beginWord != endWord
  • All the words in wordList are unique.
  • The sum of all shortest transformation sequences does not exceed 105.

Approaches

Checkout 3 different approaches to solve Word Ladder II. Click on different approaches to view the approach and algorithm in detail.

Brute-Force BFS with Path Tracking

This approach uses a Breadth-First Search (BFS) to explore the word transformations level by level. To reconstruct the paths, the entire path from the beginWord to the current word is stored in the BFS queue. When a path reaches the endWord, it's added to a result list. The search continues only up to the level where the first shortest path is found to ensure all collected paths are of minimum length.

Algorithm

  • Initialize a queue to store paths (lists of words). Add the initial path [beginWord].
  • Convert wordList to a Set for efficient O(1) lookups.
  • Perform a level-by-level Breadth-First Search (BFS).
  • In each level, process all paths currently in the queue.
  • For each path, take the last word and generate all its one-letter-different neighbors.
  • If a neighbor is the endWord, a shortest path is found. Add this complete path to the results list and set a flag found to true.
  • If a neighbor is a valid word in the dictionary and hasn't been visited in the current level, create a new path by appending the neighbor and add it to the queue. Keep track of these newly visited words in a visitedInLevel set.
  • After a level is fully processed, if found is true, terminate the search.
  • Otherwise, remove all words in visitedInLevel from the main dictionary set to prevent cycles and longer paths. Continue to the next level.

This method directly translates the problem into a pathfinding search on a graph. We treat each word as a node and a single-letter difference as an edge.

The core of this approach is a BFS, which naturally finds the shortest path in terms of the number of transformations (edges). The main challenge is to find all shortest paths. To do this, we store the entire transformation sequence (path) in our BFS queue instead of just the current word.

Here's the process:

  1. We start with a queue containing a single path: [beginWord].
  2. We process the search level by level. For each level, we dequeue all paths of that length.
  3. For each path, we look at the last word and find all its valid neighbors (one letter difference, present in the wordList).
  4. If a neighbor is the endWord, we've found a shortest path. We add the full path to our results. We make a note that we've found a solution, which means we don't need to explore any deeper levels.
  5. If a neighbor is another valid word, we create a new path by extending the current one and add it to the queue for the next level's processing.
  6. A crucial step for correctness and efficiency is to avoid cycles and moving backward. We do this by keeping track of all words visited at the current level. After the level is complete, we remove all these words from our dictionary set. This ensures that in subsequent (longer) paths, we don't revisit these words.

While conceptually simple, this approach is highly inefficient as the number of paths can grow exponentially, leading to excessive memory and time consumption.

class Solution {
    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        List<List<String>> results = new ArrayList<>();
        Set<String> dict = new HashSet<>(wordList);
        if (!dict.contains(endWord)) {
            return results;
        }

        Queue<List<String>> queue = new LinkedList<>();
        List<String> initialPath = new ArrayList<>();
        initialPath.add(beginWord);
        queue.offer(initialPath);

        Set<String> visitedInLevel = new HashSet<>();
        visitedInLevel.add(beginWord);

        boolean found = false;

        while (!queue.isEmpty()) {
            int levelSize = queue.size();
            // Words visited in the current level are removed from the main dict
            // after the level is processed to avoid cycles and longer paths.
            for (String word : visitedInLevel) {
                dict.remove(word);
            }
            visitedInLevel.clear();

            if (found) break; // Stop if shortest paths were found in the previous level

            for (int i = 0; i < levelSize; i++) {
                List<String> currentPath = queue.poll();
                String lastWord = currentPath.get(currentPath.size() - 1);

                char[] chars = lastWord.toCharArray();
                for (int j = 0; j < chars.length; j++) {
                    char originalChar = chars[j];
                    for (char c = 'a'; c <= 'z'; c++) {
                        chars[j] = c;
                        String newWord = new String(chars);

                        if (dict.contains(newWord)) {
                            List<String> newPath = new ArrayList<>(currentPath);
                            newPath.add(newWord);
                            if (newWord.equals(endWord)) {
                                results.add(newPath);
                                found = true;
                            } else {
                                visitedInLevel.add(newWord);
                                queue.offer(newPath);
                            }
                        }
                    }
                    chars[j] = originalChar; // backtrack
                }
            }
        }
        return results;
    }
}

Complexity Analysis

Time Complexity: O(N * L * b^d)Space Complexity: O(N * d * b^d)

Pros and Cons

Pros:
  • Conceptually simple and a direct implementation of the problem statement.
  • Directly builds the final paths during the search process.
Cons:
  • Extremely high memory usage because the queue stores complete paths, which can grow exponentially.
  • High time complexity due to redundant computations. If multiple paths reach the same intermediate word, the search from that word is repeated for each path.
  • Very likely to cause 'Time Limit Exceeded' or 'Memory Limit Exceeded' errors on non-trivial test cases.

Code Solutions

Checking out 2 solutions in different languages for Word Ladder II. Click on different languages to view the code.


Video Solution

Watch the video walkthrough for Word Ladder II



Algorithms:

Breadth-First Search

Patterns:

Backtracking

Data Structures:

Hash TableString

Companies:

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.