1. 题目描述
给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回一个空列表。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出:
[
[“hit”,“hot”,“dot”,“dog”,“cog”],
[“hit”,“hot”,“lot”,“log”,“cog”]
]
示例 2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: []
解释: endWord “cog” 不在字典中,所以不存在符合要求的转换序列。
2. 思路
想了三天,参考了一下别人的思路,试了好几遍,终于AC了。
每一个代码的具体目的都在代码里作了注释。
这一题是单词接龙的进阶版,要返回所有的转换序列。这里借鉴了南郭子綦的思路。
以示例1为例:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
具体来说,主要做了以下几部分的工作:
- 用字典建立beginWord和endWord之间单词的联系,其中字典的key是后面的单词,val是key前面的单词所构成的单词数组。如:hot的前一个单词是hit。当key为beginWord时,val为[],这个例子最终建立的字典是memories = {“cog”:[“log”,“dog”], “log”:[“lot”], “dog”:[“dot”], “dot”:[“hot”], “lot”:[“hot”], “hot”:[“hit”],“hit”:[]}。
- 根据构建好的字典,利用带有回溯的dfs方法生成从beginWord到endWord的转化路径。
- 在构建字典的时候,我们可以利用层次遍历的方法,借助两个集合,preset和curset。其中preset集合里是前一层的单词,curset表示当前层的单词集合。即curset中的单词是key,preset里的单词是value,当开始循环的时候,这一次循环的curset是下一层循环的preset,为了避免重复,每循环完一次,都要把curset里的单词从wordList中去掉(或者循环之前把preset里的单词去掉)。
2.1 python代码
class Solution:
def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
# 利用dfs构建路径
def buildPath(path,word):
# dfs从字典中构建出beginWord到endWord的路径,其中这个路径生成函数是从endWord往前倒推的
if len(memories[word]) == 0:
# 因为如果字典构建完成的话,在一整条完整的路径中,
# 只有beginWord的val是[],其他的都至少有一个单词,
# 否则连不成一个完整的路径。
# 这是需要将word添加进path头部(因为是倒推的)然后压入结果中
res.append([word] + path)
return
# 如果不是beginWord,将其插入path头部,然后进入这个单词的上一层单词。
# 因为可能有多个单词变换一个字母得到当前单词,利用一个for循环,
# 从字典中读取这个单词的上一层单词,分别进行递归。
path.insert(0,word)
for element in memories[word]:
buildPath(path,element)
# 由于路径可能不止一条(某个单词的上层单词有多个,或者某个单词的下层单词有多个)
# 因此需要将插入头部的元素拿出来。(回溯),每运行(递归)一次,就拿出一个元素。
# 令path恢复到for循环之前的path。
path.pop(0)
# 二: 构建字典
memories = {}
memories[beginWord] = [] # 将beginWord的val值设置成[]
wordList = set(wordList) # 因为不会重复,先对wordList进行降重。
wordList.discard(beginWord) # 将beginWord从wordList中删除
# 先初始化memories,使其key为wordList中的单词,val统一先设置成[]
for i in wordList:
memories[i] = []
# 借助两个集合(preset和curset)存储上一层单词和目前层的单词
curset = set()
curset.add(beginWord)
preset = set()
lenth = len(beginWord)
res = []
while True:
preset = curset # 上一次循环的curset变成这一次循环的precut。
# 由于curset是存储这一次循环的当前层元素的,因此每次循环前,
# 都要将curset重新清空,以便填充当前层元素
curset = set()
# 对在上一层中的单词进行判断,看是否有还在wordList中的单词,
# 可以由上一层单词一次变化得到。有的话,就修改字典中的相应key的val值。
# 同时将这个单词压入curset(当前层)
for preword in preset:
for i in range(lenth):
left = preword[:i]
right = preword[i+1:]
for byte in "abcdefghijklmnopqrstuvwxyz":
if byte != preword[i]:
tempword = left + byte + right
if tempword in wordList:
memories[tempword].append(preword)
curset.add(tempword)
# 为了避免重复,每检查完一次,都需要将当前层单词从wordList中删除,
# 要不然下一次循环的上一层元素,不能出现重复使用的情况。
for word in curset:
wordList.discard(word)
# 当构建字典的过程中,如果上一层的所有元素都无法通过一次変换
# 得到wordList中的单词),说明这个路径就会打断,即找不到从begin到end的路径
# 不用再检查,直接返回[]。当然在字典中可能会有其他的单词的val为空,
# 只要不在路径上就不影响。(不在在precut里)
if len(curset) == 0:
return []
# 如果endWord在curset中,即endWord是当前循环的当前层单词,
# 说明已经到达endWord了,打断循环即可。endWord不会是任何单词的上一层元素。
if endWord in curset:
break
buildPath([],endWord) #将字典构建成路径结果返回
return res