Word Ladder II

Leetcode Daily Challenge

Posted by Haoran on July 25, 2021

Problem

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

Constraints

1 <= beginWord.length <= 5
endWord.length == beginWord.length
1 <= wordList.length <= 1000
wordList[i].length == beginWord.length
beginWord, endWord, and wordList[i] consist of lowercase English letters.
beginWord != endWord
All the words in wordList are unique.\

Thinking

  • So a big idea is this has to be backtracking, we have to try before we know which ladder works.

  • But the problem is how to choose what word next:
    • is there a way to determine one letter difference?
    • The contraint that is useful here is the word at most have 5 letters. So if we try all possible ways, there are 5 * 24 = 120 ways for each word.
    • If we can form a one letter difference word and find if the word is in the word list, then we could continue on the path.
  • We could use dfs to search for a path, but since the problems asks for shortest path, we need to use bfs.
    • A problem is how to store what we used on the way, cause we can’t make the ladder goes back
    • Create a dict of sets. For each new word, update the key in the map to map to current set.
    • But we also need to store an array to remember the sequence
  • I realize this actually doesn’t work. Two paths can be different at first, but merge in the end. So we can’t use the word as key to store set and list.

  • A simple way to do this is actually storing the whole path in set, but that would create memory exceed
    • To optimize it, we can create a data structure to store the paths
    • In this way, the queue won’t store as many items as before, rather it stores fewer ladder object
    • The data structure needs to
      • remember all the paths sharing the same last word
    • reference: https://fizzbuzzed.com/top-interview-questions-4/
  • Create a state variable to store whether we have reached the end

  • This question is definitely a hard one. Although it only adds a bit more requirement than the word_ladder I. It’s definitely a tough problem if met in interview.
    • Familiar with BFS and DFS will definitely help
    • Graph algorithm remains a challenge to me
from typing import List
from collections import deque, defaultdict
import copy


class WordLadder:
    def __init__(self, begin_word):
        self.ladder = [[begin_word]]

    def get_last_word(self):
        return self.ladder[0][-1]

    def merge_ladder(self, other):
        self.ladder += other.ladder

    def append_word(self, word):
        for path in self.ladder:
            path.append(word)

    def get_ladder(self):
        return self.ladder


def findLadders(beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
    queue = deque([WordLadder(beginWord)])

    # To avoid circling back to seen words
    seen = set([beginWord])
    word_list = set(wordList)

    final_ladder = None

    while queue:
        n = len(queue)
        # Keep a dictionary of word mapping to ladder
        seen_this_level = {}

        for i in range(n):
            ladder = queue.popleft()
            for candidate in generate_neighbor(ladder.get_last_word(), wordList):
                new_ladder = copy.deepcopy(ladder)
                new_ladder.append_word(candidate)
                if candidate == endWord:
                    if final_ladder:
                        final_ladder.merge_ladder(new_ladder)
                    else:
                        final_ladder = new_ladder
                elif candidate in seen_this_level:
                    seen_this_level[candidate].merge_ladder(new_ladder)
                else:
                    seen_this_level[candidate] = new_ladder
                    queue.append(new_ladder)
        if final_ladder:
            break
        seen |= seen_this_level.keys()
    return final_ladder.get_ladder() if final_ladder else []
# neighbor generator


def generate_neighbor(word, wordList):
    for i in range(len(word)):
        for letter in "abcdefghijklmnopqrstuvwxyz":
            new_word = word[:i] + letter + word[i+1:]
            if new_word in wordList:
                yield new_word


if __name__ == "__main__":
    print(findLadders("hit",
                      "cog",
                      ["hot", "dot", "dog", "lot", "log", "cog"]))