- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出: 5 解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
示例 2:
输入: beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log"] 输出: 0 解释: endWord "cog" 不在字典中,所以无法进行转换。
双向广度优先搜索
本题要求的是最短转换序列的长度,看到最短首先想到的就是广度优先搜索。想到广度优先搜索自然而然的就能想到图,但是本题并没有直截了当的给出图的模型,因此我们需要把它抽象成图的模型。
我们可以把每个单词都抽象为一个点,如果两个单词可以只改变一个字母进行转换,那么说明他们之间有一条双向边。因此我们只需要把满足转换条件的点相连,就形成了一张图。
基于该图,我们以 beginWord
为图的起点,以 endWord
为终点进行广度优先搜索,寻找 beginWord
到 endWord
的最短路径。
根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 222 为底数呈指数增长。
如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord
开始,另一边从 endWord
开始。我们每次从两边各扩展一层节点,当发现某一时刻两边都访问过同一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而提高代码运行效率。
Python
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> Union[int, float]:
def addWord(word: str):
if word not in wordId:
nonlocal nodeNum
wordId[word] = nodeNum
nodeNum += 1
def addEdge(word: str):
addWord(word)
id1 = wordId[word]
chars = list(word)
for i in range(len(chars)):
tmp = chars[i]
chars[i] = '*'
newWord = ''.join(chars)
addWord(newWord)
id2 = wordId[newWord]
edge[id1].append(id2)
edge[id2].append(id1)
chars[i] = tmp
wordId, nodeNum = dict(), 0
edge = collections.defaultdict(list)
for word in wordList:
addEdge(word)
addEdge(beginWord)
if endWord not in wordId:
return 0
disBegin = [float("inf")] * nodeNum
beginId = wordId[beginWord]
disBegin[beginId] = 0
queBegin = collections.deque([beginId])
disEnd = [float("inf")] * nodeNum
endId = wordId[endWord]
disEnd[endId] = 0
queEnd = collections.deque([endId])
while queBegin or queEnd:
queBeginSize = len(queBegin)
for _ in range(queBeginSize):
nodeBegin = queBegin.popleft()
if disEnd[nodeBegin] != float("inf"):
return (disBegin[nodeBegin] + disEnd[nodeBegin]) // 2 + 1
for it in edge[nodeBegin]:
if disBegin[it] == float("inf"):
disBegin[it] = disBegin[nodeBegin] + 1
queBegin.append(it)
queEndSize = len(queEnd)
for _ in range(queEndSize):
nodeEnd = queEnd.popleft()
if disBegin[nodeEnd] != float("inf"):
return (disBegin[nodeEnd] + disEnd[nodeEnd]) // 2 + 1
for it in edge[nodeEnd]:
if disEnd[it] == float("inf"):
disEnd[it] = disEnd[nodeEnd] + 1
queEnd.append(it)
return 0