You are given two words, beginWord and endWord, and also a list of words wordList. All of the given words are of the same length, consisting of lowercase English letters, and are all distinct.
Your goal is to transform beginWord into endWord by following the rules:
beginWord to any word within wordList, provided that at exactly one position the words have a different character, and the rest of the positions have the same characters.Return the minimum number of words within the transformation sequence needed to obtain the endWord, or 0 if no such sequence exists.
Example 1:
Input: beginWord = "cat", endWord = "sag", wordList = ["bat","bag","sag","dag","dot"]
Output: 4Explanation: The transformation sequence is "cat" -> "bat" -> "bag" -> "sag".
Example 2:
Input: beginWord = "cat", endWord = "sag", wordList = ["bat","bag","sat","dag","dot"]
Output: 0Explanation: There is no possible transformation sequence from "cat" to "sag" since the word "sag" is not in the wordList.
Constraints:
1 <= beginWord.length <= 101 <= wordList.length <= 100
You should aim for a solution with O((m ^ 2) * n) time and O((m ^ 2) * n) space, where n is the number of words and m is the length of the word.
Consider the given problem in terms of a graph, treating strings as nodes. Think of a way to build edges where two strings have an edge if they differ by a single character. A naive approach would be to consider each pair of strings and check whether an edge can be formed. Can you think of an efficient way? For example, consider a string hot and think about the strings that can be formed from it by changing a single letter.
To efficiently build edges, consider transforming each word into intermediate states by replacing one character with a wildcard, like *. For example, the word hot can be transformed into *ot, h*t, and ho*. These intermediate states act as "parents" that connect words differing by one character. For instance, *ot can connect to words like cot. For each word in the list, generate all possible patterns by replacing each character with * and store the word as a child of these patterns. We can run a BFS starting from the beginWord, visiting other words while avoiding revisiting by using a hash set.
When visiting a node during BFS, if the word matches the endWord, we immediately return true. Otherwise, we generate the pattern words that can be formed from the current word and attempt to visit the words connected to these pattern words. We add only unvisited words to the queue. If we exhaust all possibilities without finding the endWord, we return false.
This problem can be modeled as finding the shortest path in an unweighted graph where each word is a node and edges connect words that differ by exactly one character. We precompute the adjacency list by comparing all pairs of words. BFS naturally finds the shortest path because it explores all nodes at distance k before any node at distance k+1.
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if (endWord not in wordList) or (beginWord == endWord):
return 0
n, m = len(wordList), len(wordList[0])
adj = [[] for _ in range(n)]
mp = {}
for i in range(n):
mp[wordList[i]] = i
for i in range(n):
for j in range(i + 1, n):
cnt = 0
for k in range(m):
if wordList[i][k] != wordList[j][k]:
cnt += 1
if cnt == 1:
adj[i].append(j)
adj[j].append(i)
q, res = deque(), 1
visit = set()
for i in range(m):
for c in range(97, 123):
if chr(c) == beginWord[i]:
continue
word = beginWord[:i] + chr(c) + beginWord[i + 1:]
if word in mp and mp[word] not in visit:
q.append(mp[word])
visit.add(mp[word])
while q:
res += 1
for i in range(len(q)):
node = q.popleft()
if wordList[node] == endWord:
return res
for nei in adj[node]:
if nei not in visit:
visit.add(nei)
q.append(nei)
return 0Where is the number of words and is the length of the word.
Instead of precomputing the entire adjacency graph, we can generate neighbors on the fly. For each word, we try replacing each character with all 26 letters. If the resulting word exists in our word set, it is a valid neighbor. This approach trades precomputation time for potentially more neighbor generation during BFS.
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if (endWord not in wordList) or (beginWord == endWord):
return 0
words, res = set(wordList), 0
q = deque([beginWord])
while q:
res += 1
for _ in range(len(q)):
node = q.popleft()
if node == endWord:
return res
for i in range(len(node)):
for c in range(97, 123):
if chr(c) == node[i]:
continue
nei = node[:i] + chr(c) + node[i + 1:]
if nei in words:
q.append(nei)
words.remove(nei)
return 0Where is the number of words and is the length of the word.
We can use wildcard patterns to efficiently group words that are one character apart. For each word, create patterns by replacing each character with a wildcard. Words sharing the same pattern are neighbors. This precomputation allows O(1) neighbor lookup during BFS, as we only need to check the pattern buckets.
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if endWord not in wordList:
return 0
nei = collections.defaultdict(list)
wordList.append(beginWord)
for word in wordList:
for j in range(len(word)):
pattern = word[:j] + "*" + word[j + 1 :]
nei[pattern].append(word)
visit = set([beginWord])
q = deque([beginWord])
res = 1
while q:
for i in range(len(q)):
word = q.popleft()
if word == endWord:
return res
for j in range(len(word)):
pattern = word[:j] + "*" + word[j + 1 :]
for neiWord in nei[pattern]:
if neiWord not in visit:
visit.add(neiWord)
q.append(neiWord)
res += 1
return 0Where is the number of words and is the length of the word.
Standard BFS explores exponentially more nodes as distance increases. By running two BFS searches simultaneously from beginWord and endWord, we can meet in the middle, effectively halving the search depth and dramatically reducing the search space. At each step, we expand the smaller frontier to balance the workload.
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
if endWord not in wordList or beginWord == endWord:
return 0
m = len(wordList[0])
wordSet = set(wordList)
qb, qe = deque([beginWord]), deque([endWord])
fromBegin, fromEnd = {beginWord: 1}, {endWord: 1}
while qb and qe:
if len(qb) > len(qe):
qb, qe = qe, qb
fromBegin, fromEnd = fromEnd, fromBegin
for _ in range(len(qb)):
word = qb.popleft()
steps = fromBegin[word]
for i in range(m):
for c in range(97, 123):
if chr(c) == word[i]:
continue
nei = word[:i] + chr(c) + word[i + 1:]
if nei not in wordSet:
continue
if nei in fromEnd:
return steps + fromEnd[nei]
if nei not in fromBegin:
fromBegin[nei] = steps + 1
qb.append(nei)
return 0Where is the number of words and is the length of the word.