[总结:广度优先搜索bfs] 752. 打开转盘锁

1,296 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

每日刷题 2022.07.30

题目

  • 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
  • 锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
  • 列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
  • 字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例

  • 示例1
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
  • 示例2
输入: deadends = ["8888"], target = "0009"
输出:1
解释:把最后一位反向旋转一次即可 "0000" -> "0009"
  • 示例3
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:无法旋转到目标数字且不被锁定。

提示

  • 1 <= deadends.length <= 500
  • deadends[i].length == 4
  • target.length == 4
  • target 不在 deadends 之中
  • target 和 deadends[i] 仅由若干位数字组成

解题思路

  • 根据题意分析:从初始的'0000'节点开始,可以拨动转换每一个位置上的数字,且每一位上面只有0~9的数值可以选择,并且转换的字符串不能是出现在deadends数组中(因为存在这个数组中的字符串会导致这个锁🔒锁住,不能再转动,也就不能到达目标节点)。
  • 询问:从开始节点到目标节点的解锁过程中,最小的旋转次数是多少?(注:每次只能旋转一个数字)
  • 竟然是需要求解最小的旋转次数,那么可以将其想象成一个图,对于每一个节点的来说,其都有四位,每一位可以往前或者往后变化,总的来说对于每一个字符串一共有2 * 4 = 8种情况,因此只需要对每一个节点的8种情况,进行筛选,将符合要求的放到接下来的步骤中再次循环操作,最先找到目标节点的层数,就是最小的旋转次数。
  • 因此使用广度优先搜索bfs,将开始节点放入到队列中,每次寻找下一层的节点放入,依次循环,最先找到目标节点的,就是最短最少的次数。

bfs需要注意的点

  • 循环的层数dep,如果题目中存在限定的层数127.单词接龙。那么就需要将while循环的判断条件进行改变
while(queue.length != 0 && k != 0) {
  if(k == 0) return;
} 
// 最后队列为空,都没有找到合适的,那么说明是永远也找不到,就返回-1
return -1;
  • 对于最开始直接放入队列中的开始节点,一定要进行标记,防止被重复访问,造成死循环
  • 在存储题目中所给的数组的时候,可以考虑下,如果每次都需要判断其是否存在于这个数组中,这样每次都需要遍历数组的长度,就会浪费时间。可以通过将数组转换成map或者set这样的数据结构,在查询的操作上就会将时间优化到o(1)
  • vis数组的必要性,有些时候因为路径是有长度的或者是有数值的,那么就不需要额外的vis数组再来记录了,可以通过路径的大小来判断其是否被遍历过。
  • 条件,如果在内部的if判断过每个节点是否符合的情况,那么就不要再在开头进行判断了,因为其在上一轮的内部就已经被拦截住了,因此也就不会通过函数最开始的判断条件。因此操作可以直接在上一轮的内部书写完成。

新的方法的学习

  • charAt(idx)返回字符串中指定的下标的字符

  • str.charCodeAt(idx)

    • 返回值:指定 index 处字符的 UTF-16 代码单元值的一个数字
    • 如果idx超出字符串的范围,则返回NaN
    • 如果没有指定idx,则默认为0;如果有,需要填写大于等于0的数值
  • 取余操作:一圈整个是10个数

    const up = (num + 1) % 10;
    const down = (num + 9) % 10; // 这样写就不用担心num - 1的时候,会超出,变成-1
    
  • 回顾:判断一个数的奇偶性

    if(num & 1) // num奇数
    else // num偶数
    

AC代码

/**
 * @param {string[]} deadends
 * @param {string} target
 * @return {number}
 */
var openLock = function(deadends, target) {
  //就是查找,最小的路径,无须返回路径,需要将次数返回即可
  // 广度优先搜索bfs
  // 也就是0可以转到9、1,9可以转到0、8
  // 需要特判,是否当前的节点就被锁死
  if(deadends.indexOf('0000') != -1) return -1;
  let queue = ['0000'], dep = 0, vis = new Map();
  let set = new Set();
  for(const one of deadends) {
    set.add(one);
  }
  vis.set('0000', 1);
  // 有可能会写一些,k--
  while(queue.length != 0) {
    let len = queue.length, q = [];
    for(let i = 0; i < len; i++) {
      let cur = queue[i];
      if(cur == target) return dep;
      // 对于每一个当前的元素,需要遍历其每一位进行波动
      for(let j = 0; j < 4; j++) {
        // let a = Number(cur[j]) + 1, b = Number(cur[j]) - 1;
        // if(a == 10) {
        //   a = 0;
        // }
        // if(b == -1) {
        //   b = 9;
        // }
        // 避免字符串和数字一起操作,产生错误
        const num = cur[j] - '0';
        const up = (num + 1) % 10;
        const down = (num + 9) % 10;
        // 将其塞入到队列中
        let stra = cur.substring(0, j) + up + cur.substring(j + 1);
        let strb = cur.substring(0, j) + down + cur.substring(j + 1);
        if(!set.has(stra) && !vis.has(stra)) {
          vis.set(stra, 1);
          q.push(stra);
        }
        if(!set.has(strb) && !vis.has(strb)) {
          vis.set(strb, 1);
          q.push(strb);
        }
      }
    }
    queue = q;
    dep++;
  }
  return -1;
};