一天一大 leet (126. 单词接龙 II)

时间:2022-07-25
本文章向大家介绍一天一大 leet (126. 单词接龙 II),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目(难度:困难):

给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换需遵循如下规则:

  1. 每次转换只能改变一个字母。
  2. 转换过程中的中间单词必须是字典中的单词。

说明

  • 如果不存在这样的转换序列,返回一个空列表。
  • 所有单词具有相同的长度。
  • 所有单词只由小写字母组成。
  • 字典中不存在重复的单词。
  • 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

示例

  • 示例 1 输入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出: [ ["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"] ]
  • 示例 2 输入: beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log"] 输出: [] 解释: endWord "cog" 不在字典中,所以不存在符合要求的转换序列。

抛砖引玉

1. 以起始单词为基准分别在 list 中找到与其只差一个字母在集合放到对象中;

  • 条件:与上一个基准单词只差一个字母
  • 遍历得到基准集合分别以他们为基准找到与他们只差一个字母的集合
  • 已经作为基准的单词不重复使用(优化迭代次数)
  • 每使用一次基准 level 层级增加一次(这要作为递归的终止条件)

2.使用递归找到 map 中依次的节点

  • 递归的终止条件:level 小于 minLevel
  • 满足在 minLevel 时出现 endWord 的路径才可以输出

3.简单的讲整体逻辑分为两步

  • 从起点开始遍历直到找到结束单词得到最小的层级
  • 递归遍历上面生产的 map 直到递归次数达到最小的层级
/**
 * @param {string} beginWord
 * @param {string} endWord
 * @param {string[]} wordList
 * @return {string[][]}
 */
var findLadders = function (beginWord, endWord, wordList) {
  if (!wordList.length || !wordList.includes(endWord)) return []
  var middleValue = [beginWord],
    startValue = [beginWord],
    map = new Map(),
    wordSet = new Set(wordList),
    readList = new Set(),
    visited = new Set(),
    level = 0,
    minLevel = 0,
    _result = []
  /**
   * 1. 分别得到每个单词变化一个字母后是不是在这个wordList中存在
   * 2. 从wordList清除已经出现的节点,避免节点重复生成关系树
   * 从beginWord开始查询与其只差一个字母的单词                   记录level = 0
   * 依次遍历上一个level查到的单词分别查到与其只差一个字母的单词   记录level + 1
   **/
  while (middleValue.length) {
    if (middleValue.includes(endWord)) break
    let mapList = []
    for (let index = 0; index < middleValue.length; index++) {
      if (readList.has(middleValue[index])) continue
      readList.add(middleValue[index])
      let mapListKey = []
      for (var i = 0; i < middleValue[index].length; i++) {
        for (var j = 97; j < 123; j++) {
          var Str =
            middleValue[index].slice(0, i) +
            String.fromCharCode(j) +
            middleValue[index].slice(i + 1)
          if (!wordSet.has(Str)) continue
          if (readList.has(Str)) continue
          if (!visited.has(Str)) mapListKey[mapListKey.length] = Str
          if (!mapList.includes(Str)) mapList[mapList.length] = Str
        }
      }
      map.set(
        middleValue[index],
        mapListKey.includes(endWord) ? [endWord] : mapListKey
      )
      if (mapListKey.includes(endWord)) minLevel = level + 1
    }
    level += 1
    mapList.forEach((i) => visited.add(i))
    middleValue = mapList
  }
  // 使用递归找到map中依次的节点 - 终止限制:level小于minLevel
  dfs(beginWord, _result, startValue, map, minLevel, 0)

  function dfs(beginWord, _result, startValue, map, minLevel, level) {
    if (level > minLevel) return
    if (beginWord === endWord) {
      _result.push(startValue.slice())
    }
    let words = map.get(beginWord)
    if (words && words.length) {
      for (let word of words) {
        startValue.push(word)
        level > minLevel
          ? null
          : dfs(word, _result, startValue, map, minLevel, level + 1)
        startValue.pop()
      }
    }
  }
  return _result
}

官方答案

class Solution {
    private static final int INF = 1 << 20;
    private Map<String, Integer> wordId; // 单词到id的映射
    private ArrayList<String> idWord; // id到单词的映射
    private ArrayList<Integer>[] edges; // 图的边

    public Solution() {
        wordId = new HashMap<>();
        idWord = new ArrayList<>();
    }

    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        int id = 0;
        // 将wordList所有单词加入wordId中 相同的只保留一个 // 并为每一个单词分配一个id
        for (String word : wordList) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, id++);
                idWord.add(word);
            }
        }
        // 若endWord不在wordList中 则无解
        if (!wordId.containsKey(endWord)) {
            return new ArrayList<>();
        }
        // 把beginWord也加入wordId中
        if (!wordId.containsKey(beginWord)) {
            wordId.put(beginWord, id++);
            idWord.add(beginWord);
        }

        // 初始化存边用的数组
        edges = new ArrayList[idWord.size()];
        for (int i = 0; i < idWord.size(); i++) {
            edges[i] = new ArrayList<>();
        }
        // 添加边
        for (int i = 0; i < idWord.size(); i++) {
            for (int j = i + 1; j < idWord.size(); j++) {
                // 若两者可以通过转换得到 则在它们间建一条无向边
                if (transformCheck(idWord.get(i), idWord.get(j))) {
                    edges[i].add(j);
                    edges[j].add(i);
                }
            }
        }

        int dest = wordId.get(endWord); // 目的ID
        List<List<String>> res = new ArrayList<>(); // 存答案
        int[] cost = new int[id]; // 到每个点的代价
        for (int i = 0; i < id; i++) {
            cost[i] = INF; // 每个点的代价初始化为无穷大
        }

        // 将起点加入队列 并将其cost设为0
        Queue<ArrayList<Integer>> q = new LinkedList<>();
        ArrayList<Integer> tmpBegin = new ArrayList<>();
        tmpBegin.add(wordId.get(beginWord));
        q.add(tmpBegin);
        cost[wordId.get(beginWord)] = 0;

        // 开始广度优先搜索
        while (!q.isEmpty()) {
            ArrayList<Integer> now = q.poll();
            int last = now.get(now.size() - 1); // 最近访问的点
            if (last == dest) { // 若该点为终点则将其存入答案res中
                ArrayList<String> tmp = new ArrayList<>();
                for (int index : now) {
                    tmp.add(idWord.get(index)); // 转换为对应的word
                }
                res.add(tmp);
            } else { // 该点不为终点 继续搜索
                for (int i = 0; i < edges[last].size(); i++) {
                    int to = edges[last].get(i);
                    // 此处<=目的在于把代价相同的不同路径全部保留下来
                    if (cost[last] + 1 <= cost[to]) {
                        cost[to] = cost[last] + 1;
                        // 把to加入路径中
                        ArrayList<Integer> tmp = new ArrayList<>(now); tmp.add(to);
                        q.add(tmp); // 把这个路径加入队列
                    }
                }
            }
        }
        return res;
    }

    // 两个字符串是否可以通过改变一个字母后相等
    boolean transformCheck(String str1, String str2) {
        int differences = 0;
        for (int i = 0; i < str1.length() && differences < 2; i++) {
            if (str1.charAt(i) != str2.charAt(i)) {
                ++differences;
            }
        }
        return differences == 1;
    }
}

高手在民间

var findLadders = function (beginWord, endWord, wordList) {
  const wordSet = new Set(wordList) // 将单词表存入Set,Set的查找是O(1)
  wordSet.add(beginWord) // 这个其实要不要都行
  if (!wordSet.has(endWord)) return [] // 如果单词表中没有endWord,则没有路径

  const levelMap = new Map() // 存放路径中的单词对应的level,即路径的深度
  const wordMap = new Map() // 存放路径中的单词是从哪些单词变来的
  const queue = [beginWord] // BFS需要维护一个队列,初始放入level0的beginWord
  const visited = new Set() // 存放访问过的路径节点(单词),避免重复

  let finished = false // 标识,true代表存在变到endWord的路径,至少存在

  let level = 0 // 路径的深度,初始化0

  levelMap.set(beginWord, 0) // 记录beginWord的level为0
  visited.add(beginWord) // 路径的起点就是beginWord,后面不能再出现它,存入visited

  while (queue.length) {
    // 队列空了,代表所有的节点都遍历完了
    let levelSize = queue.length // 当前level的单词个数
    level++ // 对一层层的单词进行遍历,level+1
    for (let i = 0; i < levelSize; i++) {
      // 将当前层的单词逐个出列,并逐个考察
      const word = queue.shift() // 当前出列的单词
      for (let i = 0; i < word.length; i++) {
        // 遍历当前单词的所有字符
        for (let code = 97; code <= 122; code++) {
          // 将当前字符替换为26个字符,生成newWord逐个试

          const newWord =
            word.slice(0, i) + String.fromCharCode(code) + word.slice(i + 1)
          if (!wordSet.has(newWord)) continue // 如果不是单词表中的单词,continue

          if (wordMap.has(newWord)) {
            // 新单词如果已经存在于wordMap,有自己的数组
            wordMap.get(newWord).push(word) // 对应的数组推入它的“父word”,即出列的单词
          } else {
            // 新单词不存在于wordMap,初始化一个数组
            wordMap.set(newWord, [word]) // 并放入“父word”
          }

          if (visited.has(newWord)) continue // 这个新单词已经存在于路径中,避免重复走过
          if (newWord === endWord) finished = true // 遇到了endWord,说明至少存在去往endWord的路径

          levelMap.set(newWord, level) // 记录这个单词所处于的路径的深度
          queue.push(newWord) // 这个新单词是下一层的,不断让下一层的单词入列
          visited.add(newWord) // 这个新单词访问过了,记录一下
        }
      }
    }
  }
  if (!finished) return [] // 无法从beginWord到达endWord,返回一个[]
  const res = []
  dfs(res, [], beginWord, endWord, wordMap, levelMap) // dfs的入口
  return res
}
function dfs(res, path, beginWord, word, wordMap, levelMap) {
  if (word === beginWord) {
    // dfs的出口,如果dfs传入的word,已经和beginWord相同
    res.push([beginWord, ...path]) // 将完整的路径推入res数组,别忘了加上beginWord
    return // path在dfs中是引用传递,要深拷贝一下再推入res
  }
  path.unshift(word) // 将当前单词加入到path数组的开头
  if (wordMap.get(word)) {
    // 当前单词对应的“邻居单词”们
    for (let parent of wordMap.get(word)) {
      // 遍历当前单词对应的“邻居单词”们
      if (levelMap.get(parent) + 1 === levelMap.get(word)) {
        // 找出其中满足层次要求
        dfs(res, path, beginWord, parent, wordMap, levelMap) // 即找出路径中它的“父单词”,递归dfs
      }
    }
  }
  path.shift() // 回溯,撤销选择,将path数组开头的单词弹出
}

菜鸟的自白

  • 最早我 map 我使用 OBject,set 我使用 Array,结果运行各种超时,检查逻辑没有问题,使用 map 和 set 优化了才通过了
  • 尽快能再生成节点 map 是减少每个节点了子节点,不然递归中数量会很大
  • 使用引用类型数据传值要注意避免操作原数据