0127 - Word Ladder (Hard)
Problem Link
https://leetcode.com/problems/word-ladder/
Problem Statement
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
for1 <= i <= k
is inwordList
. Note thatbeginWord
does not need to be inwordList
. sk == endWord
Given two words, beginWord
and endWord
, and a dictionary wordList
, return the number of words in the shortest transformation sequence from beginWord
to endWord
, or 0
if no such sequence exists.
Example 1:
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Explanation: One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> cog", which is 5 words long.
Example 2:
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
Output: 0
Explanation: The endWord "cog" is not in wordList, therefore there is no valid transformation sequence.
Constraints:
1 <= beginWord.length <= 10
endWord.length == beginWord.length
1 <= wordList.length <= 5000
wordList[i].length == beginWord.length
beginWord
,endWord
, andwordList[i]
consist of lowercase English letters.beginWord != endWord
- All the words in
wordList
are unique.
Approach 1: BFS
Imaginate is a starting node and is the ending node of a graph with some other nodes between them. The node is connected to each other by only one letter difference. The problem is same as "finding the shortest path between the starting node and the ending node in an undirected and unweighted graph".
Since we are looking for the shortest path, then we should use BFS instead of DFS. Before that, we need to pre-process on the words of the given . If a word is , then there could be three forms which are , , and where can be any alphabet. To implement this, we can use a hash map where the key is the form and the value is the list of word which has the same form. Example: .
Graph:
hit----hot---dot---dog---cog
| / | /
| / | /
lot----------log
- C++
- Python
// 1. build the combination of words that can be formed
int m = (int) beginWord.size();
map<string, vector<string>> comb;
int n = (int) wordList.size();
for (auto s : wordList) {
for (int i = 0; i < m; i++) {
string t = s.substr(0, i) + '*' + s.substr(i + 1);
comb[t].push_back(s);
}
}
wordList.append(beginWord)
# word map will be formatted in the format key:value
# key: combinations of words, ie hit: --> '_it', 'h_t', 'hi_'
# value: word from word list, 'hit'
word_map = defaultdict(list)
for word in wordList:
for i in range(len(word)):
string = word[:i] + '_' + word[i + 1:]
word_map[string].append(word)
Then, we can perform BFS using queue. We take the word, build its form and get the list of next nodes. If the next word is , then we can return the answer which is . Otherwise, we check if the next word is visited or not, then push it to the queue and mark it as visited if it hasn't been reached before.
- C++
- Python
// 2. BFS
queue<pair<string, int>> q; // {word, level}
// check if it is visited already
unordered_map<string, int> vis;
// push the first node with level 1
q.push({beginWord, 1});
// mark it visited so we won't visit it again
vis[beginWord] = 1;
while (!q.empty()) {
// get the first pair
auto [s, level] = q.front(); q.pop();
for (int i = 0; i < m; i++) {
// build the form
string t = s.substr(0, i) + '*' + s.substr(i + 1);
// iterate each possible words
for (auto w : comb[t]) {
// if it reaches the ending node, then we got the answer
if (w == endWord) {
return level + 1;
}
// else if it is not visited, then mark it visited
// and push it to the queue for next traversal
if (!vis.count(w)) {
vis[w] = 1;
q.push({w, level + 1});
}
}
}
}
# visited set initialized with beingWord
visited = set([beginWord])
# queue formatted as (word, level) where the level.
q = deque([(beginWord, 1)])
while q:
# get first word, level in queue.
word, level = q.popleft()
# check if we reached the end node, endWord.
if word == endWord:
return level
# iterated each character in word
for i in range(len(word)):
# recreate all words options ie hit: _it, h_t, hi_
string = word[:i] + '_' + word[i + 1:]
# all word combinations are our keys to our hash map, use those
# to access the word lists held inside our map.
for new_word in word_map[string]:
# if we haven't visited the word
if new_word not in visited:
# add word to visited so we don't return twice, if we
# visited at a later time, we know it will take longer
# to reach the end node from there anyway, and add to queue.
visited.add(new_word)
q.append((new_word, level + 1))
Full version
Time Complexity: where is the length of each word, and is the total number of words in .
- For each word in word list, we iterate each character, and to recreate each combination of each word to add to our hash map takes time ->
- BFS might traverse every node , and similarily, we must check each character and recreated the intermediate words -->
Space Complexity: . The space will be dominated by our hash map as each word , has combinations of words which will be our keys, and each key will have the original word of sized as a value. Meaning each word will need space, and we need to do that for words.
- C++
- Python
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
// 1. build the combination of words that can be formed
int m = (int) beginWord.size();
map<string, vector<string>> comb;
int n = (int) wordList.size();
for (auto s : wordList) {
for (int i = 0; i < m; i++) {
string t = s.substr(0, i) + '*' + s.substr(i + 1);
comb[t].push_back(s);
}
}
// 2. BFS
queue<pair<string, int>> q; // {word, level}
unordered_map<string, int> vis;
q.push({beginWord, 1});
vis[beginWord] = 1;
while (!q.empty()) {
auto [s, level] = q.front(); q.pop();
for (int i = 0; i < m; i++) {
string t = s.substr(0, i) + '*' + s.substr(i + 1);
for (auto w : comb[t]) {
if (w == endWord) {
return level + 1;
}
if (!vis.count(w)) {
vis[w] = 1;
q.push({w, level + 1});
}
}
}
}
return 0;
}
};
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
# Early termination, endWord isn't in wordList, return 0
if endWord not in wordList:
return 0
# Add all words in wordlist to defaultdict
# Ex word hit, keys: _it, h_t, hi_, value: hit
wordList.append(beginWord)
word_map = defaultdict(list)
for word in wordList:
for i in range(len(word)):
string = word[:i] + '_' + word[i + 1:]
word_map[string].append(word)
# BFS -> visited set to prevent visiting same word twice.
visited = set([beginWord])
# Queue formatted as (word, level)
q = deque([(beginWord, 1)])
while q:
word, level = q.popleft()
if word == endWord:
return level
for i in range(len(word)):
string = word[:i] + '_' + word[i + 1:]
for new_word in word_map[string]:
if new_word not in visited:
visited.add(new_word)
q.append((new_word, level + 1))
return 0