126. Word Ladder II
Hard
Given two words (beginWord and endWord), and a dictionary's word list, find all shortest transformation sequence(s) from beginWord to endWord, such that:
- Only one letter can be changed at a time
- Each transformed word must exist in the word list. Note that beginWord is not a transformed word.
Note:
- Return an empty list if there is no such transformation sequence.
- All words have the same length.
- All words contain only lowercase alphabetic characters.
- You may assume no duplicates in the word list.
- You may assume beginWord and endWord are non-empty and are not the same.
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"]
]
Example 2:
Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
Output: []
Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
笔记:
参考:https://www.youtube.com/watch?v=PblfQrdWXQ4
方法1:先使用广搜的方法,搭建起从beginWord到endWord的树。一次性处理完一整层以后,才跳到下一层去,当访问到了endWord的时候,标记一下找到了endWord,此时当遍历完这一层以后,下一层就不需要遍历了,因为即便其他路径能找到endWord,但是路径的长度肯定不是最短的了。当遍历当前层的时候,用一个next_layer_set去记录下一层的节点有哪些,同时记录当前层的节点的子节点是谁,也就是children这个字典。在当前层遍历的时候,新生成的节点不能是之前路径的节点或者当前层的任意一个节点,所以在处理完一层的时候,需要删除word_set里面当前层的所有节点。一个节点可能有多个children,当某个节点的下一个子节点是endWord的时候,这个节点的所有子节点清空,然后只保留endWord,因为这个节点如果再通过其他的子节点到达endWord的时候,路径肯定不是最短的了。
在创建完这课树以后,从beginWord开始使用dfs去得到路径。
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
word_set = set(wordList)
if endWord not in word_set:
return []
children = {}
same_layer_set = set([])
same_layer_set.add(beginWord)
next_layer_set = set([])
rlt = []
find = False
while same_layer_set and not find:
for word in same_layer_set:
if word in word_set:
word_set.remove(word)
for current_word in same_layer_set:
p = current_word
for j in range(len(beginWord)):
for c in "abcdefghijklmnopqrstuvwxyz":
new_word = p[:j] + c + p[j + 1:]
if new_word in word_set:
if not children.get(p):
children[p] = []
next_layer_set.add(new_word)
children[p].append(new_word)
if new_word == endWord:
find = True
children[p] = [endWord]
same_layer_set = next_layer_set
next_layer_set = set([])
if find:
#print(children)
def get_path(parent, path):
if parent == endWord:
path.append(endWord)
rlt.append(copy.deepcopy(path))
path.pop()
return
if not children.get(parent):
return
path.append(parent)
for child in children[parent]:
get_path(child, path)
path.pop()
get_path(beginWord, [])
return rlt
方法2:
该方法与方法1的思路基本相同,不同的是,方法1维护的是当前节点的子节点是谁,而方法2维护的则是当前节点的父节点是谁。最后找路径的时候同样是使用dfs,不过是从endWord开始倒着往前找,一直到beginWord。
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
word_set = set(wordList)
if endWord not in word_set:
return []
steps = {beginWord: 1}
parents = {}
q = collections.deque()
q.append(beginWord)
rlt = []
l = len(beginWord)
step = 0
find = False
while q and not find:
step += 1
q_length = len(q)
for i in range(q_length):
p = q.popleft()
if p in word_set:
word_set.remove(p)
for j in range(l):
for c in "abcdefghijklmnopqrstuvwxyz":
new_word = p[:j] + c + p[j + 1:]
if new_word == endWord:
find = True
if not parents.get(endWord):
parents[endWord] = []
parents[endWord].append(p)
else:
if new_word in word_set:
if not parents.get(new_word):
parents[new_word] = []
q.append(new_word)
steps[new_word] = step
if step <= steps[new_word]:
parents[new_word].append(p)
step += 1
if find:
def get_path(word, path):
if not parents.get(word):
path.append(word)
temp_path = copy.deepcopy(path)
temp_path.reverse()
rlt.append(temp_path)
path.pop()
return
path.append(word)
for temp_word in parents[word]:
get_path(temp_word, path)
path.pop()
get_path(endWord, [])
return rlt
方法3:使用双广(从两边向中间搜索)加速
假设一个满二叉树的层数是n,如果使用单向广搜,那么效率为O(2^n),如果使用双广的话,两个方向的搜索层数减半,那么效率为2*O(2^(n/2)),也就是指数变为了一半。
这个方法的基本思想跟前面的两个是一样的,但是是从两个方向开始的。该方法在leeCode上的运行时间为116ms,而前两种在650ms左右,效率提高了很多。
这里python的小知识点,python里面 的swap可以直接这样做:a, b = b, a
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
word_set = set(wordList)
if endWord not in word_set:
return []
# 用来存储每个word的子节点是谁 比如children["hot"] = ["dot", "lot"]
children = {}
# 是否找到了最短路径
found = False
rlt = []
# backward 如果是False代表从beginWord向endWord方向扩展,如果是True,则相反
backward = False
# q1代表节点更少的那一边的set,q2代表另一边
q1 = set([beginWord])
q2 = set([endWord])
# 用来存放这一层扩展出来的新的节点
next_layer_set = set([])
while q1 and q2 and not found:
# 每次都选择搜索节点最少的那一边,为了最大化双广的效率,两边各自扩展的节点是一样的时候,效率最大
if len(q1) > len(q2):
q1, q2 = q2, q1
backward = not backward
# 当要去扩展这一层的子节点的时候,之前已经放到路径里面的节点不再访问,所以需要删去
for word in q1:
if word in word_set:
word_set.remove(word)
for word in q2:
if word in word_set:
word_set.remove(word)
# 拓展q1的所有子节点
for current_word in q1:
for j in range(len(current_word)):
for c in "abcdefghijklmnopqrstuvwxyz":
new_word = current_word[:j] + c + current_word[j + 1:]
# parent用来表示父节点,child代表子节点,规定了从beginWord向endWord方向为parent到childre
parent, child = current_word, new_word
# 这里为了简化写代码的量,我们根据backward决定新扩展的节点是父节点,还是子节点
# 如果是从beginWord到endWord,则parent是current_word, child是new_word,反之反过来
if backward:
parent, child = child, parent
# 如果新扩展出来的节点在另一个set里面,则说明两个节点相遇了,那么就找了最短路径
if new_word in q2:
found = True
if not children.get(parent):
children[parent] = []
children[parent].append(child)
else:
if new_word in word_set and not found:
# 如果这个新扩展出来的节点不在word_set里面,则说明下一层的可扩展节点,然后放到存放下一层节点的set里
next_layer_set.add(new_word)
if not children.get(parent):
children[parent] = []
children[parent].append(child)
# 这里将下一层要扩展的节点放到当前set里面,同时当前set要先清空,交换完以后,next_layer_set清空,留着下一次用
# 这里python的swap可以直接如下使用
q1.clear()
q1, next_layer_set = next_layer_set, q1
# dfs找到所有的路径
if found:
def get_path(parent, path):
if parent == endWord:
path.append(endWord)
rlt.append(copy.deepcopy(path))
path.pop()
return
if not children.get(parent):
return
path.append(parent)
for child in children[parent]:
get_path(child, path)
path.pop()
get_path(beginWord, [])
return rlt