[双向bfs、朴素bfs] 127. 单词接龙

138 阅读4分钟

每日刷题 2022.07.13

题目

  • 字典 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
  • beginWord、endWord 和 wordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有字符串 互不相同

解题思路

  • 看了三叶姐的题解后,书写的双向bfs,初次学习。

为什么要用双向bfs?

  • 因为对于本题来说,beginword的长度最长是10,那么可以由beginword产生10 * 25个新单词,也就是第一层会产生250个新单词,那么第二层就会产生250 * 10 * 25 = 62500 ~~ 6 * 10 ^ 4个新单词,第三层会产生62500 * 10 * 25 = 15625000 ~~ 15 * 10 ^ 6, 再往下的话,就会产生越来越多的新单词。
  • 随着层数的增加,产生的新的单词数增加的速度很快,有可能会出现「搜索空间爆炸」的问题。那么此时朴素的bfs会出现问题,那么就可以使用 双向bfs 来解决。
  • 区别:
    • 朴素bfs,空间取决于搜索空间中的最大宽度。一般都是从一个节点一直向下,逐层遍历,直到找到终点🏁。
    • 双向bfs,可以同时从两个方向开始搜索🔍,如果两个方向的搜索找到了相同的点,那么就意味着找到了联通起点和终点的最短路径。
  • 适用情况:有解有一定数据范围同时层级节点数量以倍数或者指数级别增长的情况,双向bfs的搜索空间通常只有朴素bfs的空间消耗的几百分之一,甚至几千分之一。 image.png image.png

双向bfs如何实现呢?

  • 创建两个队列,用来存储两个方向的开始节点,从两个方向开始遍历。
  • 创建两个哈希表{key: val} == {节点: 转换次数},用于解决相同节点重复搜索记录转换次数
  • 为了尽可能让两个搜索方向平均(这样可以防止一边倒的情况使空间爆炸),每次需要选择队列长度较短的进行拓展。
  • 如果在搜索过程中,当前的节点存在于两个哈希表中,就表示找到了最短路径,即:两个集合中存储的当前节点的转换次数相加。

注意⚠️

  • js中优化队列的操作)如果是传递参数的方式书写的bfs,就不能直接在函数内部书写qq = q,因为仅仅只是在函数内修改了qq,函数结束,就会将其销毁,并不会影响外部的qq队列。

AC代码

  • 双向bfs
/**
 * @param {string} beginWord
 * @param {string} endWord
 * @param {string[]} wordList
 * @return {number}
 */
var ladderLength = function(beginWord, endWord, wordList) {
  // 双向bfs
  // 首先先将所有列表中的数存放到集合中
  let set = new Set(wordList);
  // 不存在末尾的单词的时候,就直接返回0
  if(!set.has(endWord)) return 0;
  // 实现bfs
  let ans = bfs();
  return ans == -1 ? 0 : ans;
  
  // 双向bfs
  function bfs() {
    // 声明两个队列、两个map
    let que1 = [beginWord],que2 = [endWord];
    // Map集合记录当前这个单词,是在第几层被遍历到的,这样就不需要vis数组了
    let map1 = new Map(), map2 = new Map();
    map1.set(beginWord, 0);
    map2.set(endWord, 0);
    // 需要平衡一下两个队列的大小,让小的先找
    while (que1.length != 0 && que2.length != 0) {
      let nn = -1;
      // 不等于0才需要执行
      if(que1.length <= que2.length) {
        nn = update(que1, map1, map2);
      }else {
        nn = update(que2, map2, map1);
      }
      if(nn != -1) return nn;
    }
    return -1;
  }

  function update (q, m1, m2) {
    // 层序遍历一层,完整的
    let len = q.length;
    for(let i = 0; i < len; i++) {
      let cur = q.shift(), curLen = cur.length;
      // 循环判断修改其每一位
      for(let z = 0; z < curLen; z++) {
        for(let j = 0; j < 26; j++) {
          // 替换每一位
          let str = cur.substr(0, z) + String.fromCharCode('a'.charCodeAt() + j) + cur.substr(z + 1);
          // 需要判断当前拼接后的是否符合要求
          if(set.has(str)) {
            // 然后再进一步判断
            // 判断是否在当前方向遍历过
            if(m1.has(str)) continue;
            // 判断另一个集合中是否遍历到,如果遍历到,就直接返回
            if(m2.has(str)) {
              // 找到相遇的节点,返回结果
              // 因为步数是从0开始的,因此这里需要加上两个初始节点的步数
              return m1.get(cur) + m2.get(str) + 2;
            } else {
              // 不是相遇的节点,还需要遍历
              // q.push(str);
              m1.set(str, m1.get(cur) + 1);
            }
          }
        }
      }
    }
    // q = qq;
    return -1;
  }
};