【面试】运算器-⑬打开转盘锁

文章描述了一个关于解决转盘锁问题的编程挑战,目标是找到从初始数字“0000”到目标数字“target”的最小旋转次数,同时避开给定的“deadends”列表中的锁定数字。使用广度优先搜索策略遍历所有可能的状态,寻找最短路径。
摘要由CSDN通过智能技术生成
感谢力扣
提示

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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] 仅由若干位数字组成

1.分析题目

        根据题目的题解,我首先判断0到每一个位的距离,列表如下所示:

        目标是1,1-0=1转1次;9-1+1=9次 0-9 9-8 8-7 7-6 6-5 5-4 4-3 3-2 2-1

        目标是2,2-0=2转2次;9-2+1=8次

        目标是3,3-0=3转3次;9-3+1=7次

        目标是4,4-0=4转4次;9-4+1=6次

        目标是5,5-0=5转5次;9-5+1=5次

        目标是6,6-0=6转6次;9-6+1=4次 0-9 9-8 8-7 7-6

        目标是7,7-0=7转7次;9-7+1=3次

        目标是8,8-0=8转8次;9-8+1=2次

        目标是9,9-0=9转9次;9-9+1=1次

        可以看到如果没有deadends的时候,我们是可以根据目标是多少来直接算出来最少次数的,但是因为有deadends的数字存在,所以我们要在最短路径里来进行遍历,绕开所有的deadends,极端情况下按照上面算出来的数字,全部都在deadends的列表里面,就可能导致无法得到最优结果,因为时间原因,需要直接理解答案,步骤如下:

        ① 看题解写注释

        ② 理解广度优先搜索算法

        ③ 总结

2.学习题解

        

class Solution {
    public int openLock(String[] deadends, String target) {
        if ("0000".equals(target)) {
            return 0;
        }

        Set<String> dead = new HashSet<String>();
        for (String deadend : deadends) {
            dead.add(deadend);
        }
        if (dead.contains("0000")) {
            return -1;
        }

        int step = 0;
        Queue<String> queue = new LinkedList<String>();
        queue.offer("0000");
        Set<String> seen = new HashSet<String>();
        seen.add("0000");

        while (!queue.isEmpty()) {
            ++step;
            int size = queue.size();
            for (int i = 0; i < size; ++i) {
                String status = queue.poll();
                for (String nextStatus : get(status)) {
                    if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
                        if (nextStatus.equals(target)) {
                            return step;
                        }
                        queue.offer(nextStatus);
                        seen.add(nextStatus);
                    }
                }
            }
        }

        return -1;
    }

    public char numPrev(char x) {
        return x == '0' ? '9' : (char) (x - 1);
    }

    public char numSucc(char x) {
        return x == '9' ? '0' : (char) (x + 1);
    }

    // 枚举 status 通过一次旋转得到的数字
    public List<String> get(String status) {
        List<String> ret = new ArrayList<String>();
        char[] array = status.toCharArray();
        for (int i = 0; i < 4; ++i) {
            char num = array[i];
            array[i] = numPrev(num);
            ret.add(new String(array));
            array[i] = numSucc(num);
            ret.add(new String(array));
            array[i] = num;
        }
        return ret;
    }
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/open-the-lock/solutions/843687/da-kai-zhuan-pan-suo-by-leetcode-solutio-l0xo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

每行注释的版本如下所示:

    // cz:总的来说是每一次都把当前元素全部出队,然后把下一批元素全部入队
    public int openLock(String[] deadends, String target) {
        // cz:0000的直接返回0步达成
        if ("0000".equals(target)) {
            return 0;
        }
        // cz:定义一个哈希表变量dead用于存储deadends的所有内容
        Set<String> dead = new HashSet<String>();
        // cz:遍历所有会锁的号码,存到哈希表中
        for (String deadend : deadends) {
            // cz:添加元素
            dead.add(deadend);
        }
        // cz:如果哈希表里面有0000,一开始就满足了,直接返回-1结束
        if (dead.contains("0000")) {
            return -1;
        }

        // cz:初始化旋转的步数为0
        int step = 0;
        // cz:定义一个队列用于存储
        Queue<String> queue = new LinkedList<String>();
        // cz:把初始状态“0000”入队
        queue.offer("0000");
        // cz:定义一个哈希表用于存储是否已经检查过该元素
        Set<String> seen = new HashSet<String>();
        // cz:把第一步0000存进去,
        seen.add("0000");
        // cz:如果队列空了,那么就证明
        while (!queue.isEmpty()) {
            // cz:广度优先搜索 遍历所有一次旋转能够到的数字
            ++step;
            // cz:获取当前队列的大小以备后用
            int size = queue.size();
            // cz:从队列的第一个元素到最后一个元素进行遍历,中间加入新的元素没有关系,因为队列是先进先出,所以,
            // cz:只要明确当前队列的数量,就可以把当前队列遍历完成,和后面新增或者不新增没有直接间接欸联系;
            for (int i = 0; i < size; ++i) {
                // cz:获取队列每一个元素的值,即保险箱的当前状态
                String status = queue.poll();
                // cz:遍历通过当前状态通过一次旋转所能够旋转到的所有状态
                for (String nextStatus : get(status)) {
                    // cz:判断seen列表里面有没有当前状态 在没有的情况下,判断死锁列表里面有没有当前状态,在没有的情况下
                    if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
                        // cz:如果刚好等于目标数字
                        if (nextStatus.equals(target)) {
                            // 返回当前步骤数量
                            return step;
                        }
                        // 队列加上此状态
                        queue.offer(nextStatus);
                        // 搜索标识加上此状态
                        seen.add(nextStatus);
                    }
                }
            }
        }
        // cz:广度搜索后如果没有找到答案,返回无解
        return -1;
    }

    // cz: 输入一个字符,向下翻滚 9→8 8→7 … 1→0 0→9
    public char numPrev(char x) {
        // cz:如果当前是0不能变为-1,所以变为9
        return x == '0' ? '9' : (char) (x - 1);
    }

    // cz:输入一个字符,向上翻滚 0→1 1→2 … 8→9 9→0
    public char numSucc(char x) {
        // cz:如果当前是9不能变为10,所以变为0
        return x == '9' ? '0' : (char) (x + 1);
    }

    // 枚举 status 通过一次旋转得到的数字
    public List<String> get(String status) {
        // cz:定义一个List用于从status状态开始通过一次旋转所能得到的数字
        List<String> ret = new ArrayList<String>();
        // cz:定义一个字符数组,把当前的状态转换为可以单字符处理
        char[] array = status.toCharArray();
        // cz:因为最大是4未,所以就没有用array.lenth了,遍历状态的每个字符
        for (int i = 0; i < 4; ++i) {
            // cz:获取当前字符
            char num = array[i];
            // cz:把当前字符向下翻滚一下
            array[i] = numPrev(num);
            // cz:加入到能得到的数字的List中去
            ret.add(new String(array));
            // cz:把当前字符向上翻滚一下
            array[i] = numSucc(num);
            // cz:加入到能得到的数字的List中去
            ret.add(new String(array));
            // cz:恢复当前字符继续遍历
            array[i] = num;
        }
        // cz:返回所有通过当前状态进行一次变换所能得到的所有数字List
        return ret;
    }

        没有想到要用广度优先搜索,这个算法解决了几个关键问题:1.怎么定义步数;2.怎么保证每一次的全遍历;3.怎么把遍历过的剔除;

3.广度优先

        之前学习定义的时候是借助打火车的例子理解的,我觉得要从两个角度总结一下,第一个角度就是什么情况下用广度优先搜索算法,第二个是使用广度优先算法应该注意什么:

        使用广度优先算法的方式(自认为):a.可以被明确的拆分成可以递归的步骤;b.每动一个基础单位都需要重新按照步骤处理;c.对处理的经过不是特别关注(如果关注可能用深度优先可能会更好点);

        使用广度优先算法需要注意的(自认为):a.判定是否把当前节点所有可能的下一步遍历完全的标准;b.终止条件需要再三确认;c.关键的记录变量的修改条件;d.避免多次重复判断的过滤算法需要明确;

4.反思总结

        想起来一句话,不要拿自己的业余去挑战别人的专业,人生总是有很多分叉路口,还是得把自己的本领提高,提高自己的学习能力。哈哈,得之我幸,失之我命。

        ① 状态不好的时候应该要调整自己的状态,拖延时间会大大降低完成一件事的成就感,调整状态的方法包括但不限于洗冷水脸,睡觉,健身体操等;

        ② 世界上只有一条路,就是踏实往前走的路。

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值