【前端刷题】126.单词接龙 II(HARD)

151 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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 这样的单词序列,并满足:

  • 每对相邻的单词之间仅有单个字母不同。
  • 转换过程中的每个单词 si1 <= 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 <= 5
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 5000
  • wordList[i].length == beginWord.length
  • beginWordendWord 和 wordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有单词 互不相同

思路

  1. endWord 出发,单向 BFS 逆向构建有向无环连通图 graph
  2. beginWord 出发,普通 DFS 走出的所有路径(不重复访问节点),就是最短连通路径集合

NOTICE

  • 逆向 BFS 的时候,将一个节点标记为已访问的时机,是在 BFS 的层操作中,而不是在 BFS 的节点操作中。
    • 例子如下,BFS 从 cog 出发逆向建图,当前层有节点 dot, lot 的时候,先建立边 hot -> dot
      • 如果在节点操作中把 hot 记录为已访问,那么在尝试建立边 hot -> lot 时会跳过,所构建的图就缺少边 hot -> lot
      • 所以在节点操作中只是将边 hot -> dot, hot -> lot 记录为图的补丁,处理完一层的所有节点后,再在层操作中统一将补丁打入图中
hit - hot - dot - dog - cog
          \ lot - log /
  • 只要 beginWordgraph
    • DFS 走出的所有路径必然连通 endWord
      • 因为建图的 BFS 是从 endWord 出发,入图的各个节点必然连通 endWord
      • 如果通过双向 BFS 建图),那么 DFS 从 beginWord 出发时,所走路径有可能不连通到 endWord ,所以需要记忆化 DFS 优化(参考[题解](),否则部分测试用例会超时。
    • DFS 走出的所有路径必然最短,因为是通过 BFS 建图的,根据 BFS 的性质,图中各个节点到 endWord 的路径是最短路径

具体代码

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]
  }
}