Given two words (beginWord and endWord), and a dictionary's word list, find the length of shortest transformation sequence from beginWord to endWord, such that:
Note:
Example 1:
Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.
Example 2:
Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
Output: 0
Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.
分析:
题目意思是给定一个不重复的、元素长度相同的序列,给定起始两个单词,要求不重复地利用序列中的单词,进行初始词到结束词的最短路径。
这题蛮有意思的,整个做题过程中前前后后改了很多遍,不断超时超时又超时。。
拿到题目后我认为这是一道搜索问题,先是写了个程序进行dfs深度优先搜索,大意是每次从列表中选择一个一个单词,该单词作为新的初始词(beginWord),在剩余的列表中寻找该初始词到结束词的最短路径,在递归的过程中记录路径长度。
然而这种方式效率实在太低,通过几个case后就超时了,迫使我继续思考。后来想到,在上述递归的过程中,存在大量重复的搜索。比如某个列表[A, B, C, D],程序会分别判断列表中每个词到结束词的最短路径,但是这个过程中程序可能多次判断A到结束词的最短路径,B到结束词的最短路径等,于是我用一个列表来记录当前已经访问过的点,如果这个点确定到不了结束点,则标记下,后续不再访问该节点,效率提高了很多,但是仍然超时。。
再之后,想到用列表来记录已经访问过节点,如果一个节点已经被访问过,那么在下一次迭代中将不再访问。思路是,从结束词出发,遍历列表,如果有词能达到结束词,那么最短路径中必然包含这些结束词(中的一个或多个),之后在下一轮的迭代中,程序将排除掉这些结束词,继续以新的词为结束词,重复上述过程,直到某个词等于开始词,迭代结束,最短路径也被算出返回。举个例子,假如某个列表[A, B, C, D, E, F, G], 到达结束词EndWord 的词有[A B C],那么如果这个列表有最短路径,那么这些结束词最后必须要经过[A B C]中的某一个到达结束词,并且这些词[A B C]只会在这一层被使用,于是在下一层迭代时,可以以[A B C]为新的结束词,在删减后的列表[D, E, F, G]中寻找最短路径。这本质上是一种BFS的方法。
代码如下(注意,仍然超时):
class Solution:
def ladderLength(self, beginWord, endWord, wordList):
"""
:type beginWord: str
:type endWord: str
:type wordList: List[str]
:rtype: int
"""
cur_path = 0
if beginWord in wordList:
wordList.remove(beginWord)
if endWord not in wordList:
return 0
if endWord in wordList:
wordList.remove(endWord)
return self.dfs(beginWord, set([endWord]), set(wordList), cur_path)
def dfs(self, beginWord, endWordSet, wordSet, cur_path):
'''递归寻找最短路径'''
for ew in endWordSet: # 以endWordSet为新的结束词,来寻找这些结束词到开始词的最短路径
if self.can_change(beginWord, ew): # 如果这些词就已经能导到开始词了,则最短路径已经找到,返回
return cur_path + 2
cur_word_set = set()
should_recursion = False
for w in wordSet: # 1 对于列表中每个词
for ew in endWordSet: # 2 对于新的结束词列表中的每个词
if self.can_change(w, ew): # 3 判断是否能够转换
cur_word_set.add(w) # 4 生成新的结束词,作为下一轮的结果
should_recursion = True
if should_recursion:
return self.dfs(beginWord, cur_word_set, wordSet - cur_word_set, cur_path + 1)
return 0
def can_change(self, word1, word2):
'''两个单词之间作比较,看是否满足两个单词之间只有一个字符不同的设定'''
diff_words = 0
for i, v in enumerate(word1):
if word1[i] != word2[i]:
diff_words += 1
if diff_words > 1:
return False
if diff_words == 1:
return True
return False
上述代码依旧超时,虽然每次递归单词列表总量都在减少,但是在单词列表总长度很大时依然会超时。上述代码耗时主要提现在注释中1 和 2的双重循环中。
但是到了这个过程我已经不知道怎么去优化了,看了下LeetCode讨论区的代码,发现大家并没有使用我这种遍历判断的方法,而是按位对单词进行修改,判断修改后单词是否在单词列表中。了解到这层之后,用这种方法替换了我的双重循环,然后终于AC了。
代码如下(已AC):
class Solution:
def ladderLength(self, beginWord, endWord, wordList):
"""
:type beginWord: str
:type endWord: str
:type wordList: List[str]
:rtype: int
"""
wordSet = set(wordList)
if beginWord in wordSet:
wordSet.remove(beginWord)
if endWord in wordSet:
wordSet.remove(endWord)
else:
return 0
endWordSet = set([endWord])
return self.dfs(beginWord, endWordSet, wordSet - endWordSet, cur_path=0)
def dfs(self, beginWord, endWordSet, wordSet, cur_path):
if len(endWordSet) == 0:
return 0
cur_word_set = set()
for ew in endWordSet: # 1. 对于列表中的每个单词
for i in range(len(beginWord)): # 2. 对于单词的每一位
for l in 'abcdefghijklmnopqrstuvwxyz': # 3. 对于每一位可以替换的 26 个字母
c_w = ew[: i] + l + ew[i + 1:]
if c_w in wordSet:
cur_word_set.add(c_w)
if c_w == beginWord:
return cur_path + 2
return self.dfs(beginWord, cur_word_set, wordSet - cur_word_set, cur_path + 1)
上述代码最耗时的地方在注释的1.2.3出,但是其每次循环的次数固定,不过len(结束词列表长度) * 单词长度 * 26;而原先的双重循环,则是len(结束词列表长度) * len(总列表长度) ,新的判断方式能够极大地提高效率。
继续修改代码,将BFS的递归代码改成非递归方式。
代码如下:
class Solution:
def ladderLength(self, beginWord, endWord, wordList):
"""
:type beginWord: str
:type endWord: str
:type wordList: List[str]
:rtype: int
"""
wordSet = set(wordList)
if endWord in wordSet:
wordSet.remove(endWord)
else:
return 0
endWordSet = set([endWord])
path = 1
while True:
curWordSet = set()
for ew in endWordSet:
for i in range(len(beginWord)):
for l in 'abcdefghijklmnopqrstuvwxyz':
c_w = ew[: i] + l + ew[i + 1:]
if c_w in wordSet:
curWordSet.add(c_w)
if c_w == beginWord:
return path + 1
endWordSet = curWordSet
wordSet = wordSet - curWordSet
if len(endWordSet) == 0:
return 0
path += 1
return 0
再之后,在上面代码基础上,进行修改,并解决了WordLadderII。