一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情
题目(Word Ladder II)
链接:https://leetcode-cn.com/problems/word-ladder-ii
解决数:394
通过率:40.3%
标签:广度优先搜索 哈希表 字符串 回溯
相关公司:amazon facebook google
按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
- 每对相邻的单词之间仅有单个字母不同。
- 转换过程中的每个单词
si(1 <= i <= k)必须是字典wordList中的单词。注意,beginWord不必是字典wordList中的单词。 sk == endWord
给你两个单词 beginWord 和 endWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 **[beginWord, s1, s2, ..., sk] 的形式返回。
示例 1:
输入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出: [["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]
解释: 存在 2 种最短的转换序列:
"hit" -> "hot" -> "dot" -> "dog" -> "cog"
"hit" -> "hot" -> "lot" -> "log" -> "cog"
示例 2:
输入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出: []
解释: endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。
提示:
1 <= beginWord.length <= 5endWord.length == beginWord.length1 <= wordList.length <= 5000wordList[i].length == beginWord.lengthbeginWord、endWord和wordList[i]由小写英文字母组成beginWord != endWordwordList中的所有单词 互不相同
思路
- 从
endWord出发,单向 BFS 逆向构建有向无环连通图graph - 从
beginWord出发,普通 DFS 走出的所有路径(不重复访问节点),就是最短连通路径集合
NOTICE
- 逆向 BFS 的时候,将一个节点标记为已访问的时机,是在 BFS 的层操作中,而不是在 BFS 的节点操作中。
- 例子如下,BFS 从
cog出发逆向建图,当前层有节点dot,lot的时候,先建立边hot -> dot- 如果在节点操作中把
hot记录为已访问,那么在尝试建立边hot -> lot时会跳过,所构建的图就缺少边hot -> lot - 所以在节点操作中只是将边
hot -> dot,hot -> lot记录为图的补丁,处理完一层的所有节点后,再在层操作中统一将补丁打入图中
- 如果在节点操作中把
- 例子如下,BFS 从
hit - hot - dot - dog - cog
\ lot - log /
- 只要
beginWord在graph中- DFS 走出的所有路径必然连通
endWord- 因为建图的 BFS 是从
endWord出发,入图的各个节点必然连通endWord - 如果通过双向 BFS 建图),那么 DFS 从
beginWord出发时,所走路径有可能不连通到endWord,所以需要记忆化 DFS 优化(参考[题解](),否则部分测试用例会超时。
- 因为建图的 BFS 是从
- DFS 走出的所有路径必然最短,因为是通过 BFS 建图的,根据 BFS 的性质,图中各个节点到
endWord的路径是最短路径
- DFS 走出的所有路径必然连通
具体代码
var findLadders = function(beginWord, endWord, wordList) {
let ans = []
let wordSet = new Set(wordList)
if (!wordSet.has(endWord)) return ans
if (!wordSet.has(beginWord)) wordSet.add(beginWord)
let graph = bfsBuildGraph(beginWord, endWord, wordSet)
// 如果不连通
if (!graph) return ans
// memo[word] 表示从 word 到 endWord 的最短路径列表,路径中包括 word 自身
let memo = Object.create(null)
memo[endWord] = [[endWord]] // 用于 DFS 出口
return dfs(memo, graph, beginWord)
function bfsBuildGraph(beginWord, endWord, wordSet) {
const CODE_A = 'a'.charCodeAt(0), CODE_Z = 'z'.charCodeAt(0)
let graph = Object.create(null)
// 优化:有可能经过不同的路径,在同一层到达相同的节点,所以 queue 中节点可以去重,减少计算量
let queue1 = new Set([beginWord]), queue2 = new Set([endWord])
// 入队的时候记录为已访问
let visited1 = new Set(queue1), visited2 = new Set(queue2)
let isFromBegin = true, isConnected = false
// 只要有一个队列为空,就说明不连通
while (queue1.size && queue2.size) {
if (queue1.size > queue2.size) {
;[queue1, queue2, visited1, visited2] = [queue2, queue1, visited2, visited1]
isFromBegin = !isFromBegin
}
// 如果 isFromBegin 为 true , graphPatch[word] 是 word 的后继节点集合
let graphPatch = Object.create(null)
// BFS 节点操作
// 先找下一个节点
for (const word of queue1) { // 软出队
for (let i = 0; i < word.length; ++i) {
for (let code = CODE_A; code <= CODE_Z; ++code) {
const char = String.fromCharCode(code)
if (char === word[i]) continue // 跳过与自身相同的单词
const nextWord = word.slice(0, i) + char + word.slice(i + 1)
if (!wordSet.has(nextWord) || visited1.has(nextWord)) continue
// 如果 nextWord 连通
if (visited2.has(nextWord)) isConnected = true
// 如果 nextWord 不能连通,但之前已连通,说明 nextWord 延伸出的分支要么不能连通,要么能连通但不是最短
else if (isConnected) continue
// 入图补丁
if (!graphPatch[word]) graphPatch[word] = new Set()
graphPatch[word].add(nextWord)
}
}
}
queue1.clear() // 真实出队
// BFS 层操作,入图、入队、记录已访问
for (const word in graphPatch) {
for (const nextWord of graphPatch[word]) {
const [wordFromBegin, wordFromEnd] = isFromBegin ? [word, nextWord] : [nextWord, word]
if (!graph[wordFromBegin]) graph[wordFromBegin] = new Set()
graph[wordFromBegin].add(wordFromEnd)
if (isConnected) continue
queue1.add(nextWord)
visited1.add(nextWord)
}
}
}
// BUGFIX: 别忘了返回 graph 之前,检查是否连通
if (!isConnected) return null
return graph
}
// 返回从 cur 到 endWord 的最短路径列表,
// 如果不连通,则返回 null
function dfs(memo, graph, cur) {
// memo[cur] 有数据就直接读取;没数据就先计算数据并写入,最后再返回
if (memo[cur]) return memo[cur]
memo[cur] = []
const nextSet = graph[cur]
if (!nextSet) return memo[cur]
for (const next of nextSet) {
const succeedPathList = dfs(memo, graph, next)
// 写入 memo
if (succeedPathList) {
memo[cur].push(
// BUGFIX: 是用 `[cur]` 而不是 `path`
...succeedPathList.map(succeedPath => [cur].concat(succeedPath))
)
}
}
// BUGFIX: 别忘了返回 memo[cur]
return memo[cur]
}
}