752. 打开转盘锁
感谢力扣
提示
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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.学习题解
java
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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
每行注释的版本如下所示:
java
// 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.反思总结
想起来一句话,不要拿自己的业余去挑战别人的专业,人生总是有很多分叉路口,还是得把自己的本领提高,提高自己的学习能力。哈哈,得之我幸,失之我命。
① 状态不好的时候应该要调整自己的状态,拖延时间会大大降低完成一件事的成就感,调整状态的方法包括但不限于洗冷水脸,睡觉,健身体操等;
② 世界上只有一条路,就是踏实往前走的路。