单词接龙

标签: 广度优先搜索 哈希表 字符串

难度: Hard

字典 wordList 中从单词 beginWord endWord转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  •  对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWord endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0

 

示例 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" 不在字典中,所以无法进行转换。

提示:

  • 1 <= beginWord.length <= 10
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 5000
  • wordList[i].length == beginWord.length
  • beginWordendWordwordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有字符串 互不相同

Submission

运行时间: 136 ms

内存: 30.0 MB

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordId = {}
        edges = collections.defaultdict(list)
        nodeNum = 0
        

        def addWord(word):
            nonlocal nodeNum
            if word not in wordId:
                wordId[word] = nodeNum
                nodeNum += 1


        def addEdge(word):
            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]
                edges[id1].append(id2)
                edges[id2].append(id1)
                chars[i] = tmp


        for word in wordList:
            addEdge(word)
        addEdge(beginWord)


        if endWord not in wordId:
            return 0
        

        beginId = wordId[beginWord]
        queBegin = collections.deque([beginId])
        disBegin = [float('inf')] * nodeNum
        disBegin[beginId] = 0


        endId = wordId[endWord]
        queEnd = collections.deque([endId])
        disEnd = [float('inf')] * nodeNum
        disEnd[endId] = 0


        while queBegin or queEnd:
            nodeBeginNum = len(queBegin)
            for _ in range(nodeBeginNum):
                nodeIdBegin = queBegin.popleft()
                if disEnd[nodeIdBegin] != float('inf'):
                    return (disBegin[nodeIdBegin] + disEnd[nodeIdBegin]) // 2 + 1
                for it in edges[nodeIdBegin]:
                    if disBegin[it] == float('inf'):
                        queBegin.append(it)
                        disBegin[it] = disBegin[nodeIdBegin] + 1
            nodeEndNum = len(queEnd)
            for _ in range(nodeEndNum):
                nodeEndId = queEnd.popleft()
                if disBegin[nodeEndId] != float('inf'):
                    return (disBegin[nodeEndId] + disEnd[nodeEndId]) // 2 + 1
                for it in edges[nodeEndId]:
                    if disEnd[it] == float('inf'):
                        queEnd.append(it)
                        disEnd[it] = disEnd[nodeEndId] + 1
        

        return 0            

Explain

这个题解采用了双向广度优先搜索(BFS)的思路来解决单词接龙问题。首先,将所有单词存储在一个字典中,并为每个单词分配一个唯一的ID。然后,构建一个无向图,其中每个节点表示一个单词,如果两个单词只相差一个字母,则在它们之间添加一条边。接下来,同时从起点单词和终点单词开始进行BFS,每次搜索一层节点。如果在某一层发现起点单词的BFS和终点单词的BFS相遇,则找到了最短转换序列,返回序列的长度。如果其中一个BFS搜索完了所有节点而没有相遇,则说明不存在转换序列,返回0。

时间复杂度: O(N * L^2)

空间复杂度: O(N * L)

```python
class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordId = {}
        edges = collections.defaultdict(list)
        nodeNum = 0
        
        # 将单词加入到字典中并分配唯一ID
        def addWord(word):
            nonlocal nodeNum
            if word not in wordId:
                wordId[word] = nodeNum
                nodeNum += 1

        # 在无向图中添加单词节点和边
        def addEdge(word):
            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]
                edges[id1].append(id2)
                edges[id2].append(id1)
                chars[i] = tmp

        # 将所有单词加入无向图中
        for word in wordList:
            addEdge(word)
        addEdge(beginWord)

        # 如果endWord不在字典中,则无法进行转换
        if endWord not in wordId:
            return 0
        
        # 分别从beginWord和endWord开始进行双向BFS
        beginId = wordId[beginWord]
        queBegin = collections.deque([beginId])
        disBegin = [float('inf')] * nodeNum
        disBegin[beginId] = 0

        endId = wordId[endWord]
        queEnd = collections.deque([endId])
        disEnd = [float('inf')] * nodeNum
        disEnd[endId] = 0

        # 双向BFS搜索
        while queBegin or queEnd:
            nodeBeginNum = len(queBegin)
            for _ in range(nodeBeginNum):
                nodeIdBegin = queBegin.popleft()
                if disEnd[nodeIdBegin] != float('inf'):
                    return (disBegin[nodeIdBegin] + disEnd[nodeIdBegin]) // 2 + 1
                for it in edges[nodeIdBegin]:
                    if disBegin[it] == float('inf'):
                        queBegin.append(it)
                        disBegin[it] = disBegin[nodeIdBegin] + 1
            nodeEndNum = len(queEnd)
            for _ in range(nodeEndNum):
                nodeEndId = queEnd.popleft()
                if disBegin[nodeEndId] != float('inf'):
                    return (disBegin[nodeEndId] + disEnd[nodeEndId]) // 2 + 1
                for it in edges[nodeEndId]:
                    if disEnd[it] == float('inf'):
                        queEnd.append(it)
                        disEnd[it] = disEnd[nodeEndId] + 1
        
        # 如果两个BFS都搜索完了所有节点仍未相遇,则不存在转换序列
        return 0            
```

Explore

双向广度优先搜索(BFS)通常比单向BFS更有效,因为它从两个方向同时搜索路径,一旦两个搜索在中间某点相遇,即完成搜索。这样可以大大减少搜索空间。在单词接龙问题中,起点和终点已知,利用双向BFS可以从两端同时逼近对方,当两个搜索相遇时,即找到最短路径。这种方法在最坏情况下的时间复杂度通常优于单向BFS。

将每个单词字符替换成通配符'*'可以有效地将所有可能通过单一字符变化能相互转换的单词组合起来。这样做可以创建一个虚拟节点,该节点连接所有只有一个字母不同的单词。例如,单词'log'和'cog'都可以通过虚拟节点'*og'相连。这种方法简化了图的构建过程,使得每个单词与其可能的变种之间的关系更加明确,从而加快了搜索速度,尤其是在单词量大的情况下。

是的,endWord必须在wordList中才可能有解。在这种实现中,所有的单词都是从wordList加入图中的,如果endWord不在wordList中,则没有将其加入图中的步骤。因此,如果endWord不在wordList或者不是初始的beginWord,那么在图中找不到对应的节点,也就无法进行有效的路径搜索。这意味着找不到从beginWord到endWord的有效转换序列。

在双向BFS中,两个搜索队列分别从起点和终点出发。每进行一轮搜索时,都会检查当前扩展的节点是否已被对方搜索过。这是通过维护两个距离数组(disBegin和disEnd)来实现的,分别记录从起点和终点到每个节点的距离。每次节点扩展时,会检查该节点是否已在对方的距离数组中有记录(即距离不是无穷大)。如果条件成立,说明两边的搜索已经在这个节点相遇,从而可以计算出最短路径长度。这种机制确保了两个搜索方向在恰当的位置相遇,并能够立即识别出相遇点。