一、题目解析
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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
解释:
无法旋转到目标数字且不被锁定。
示例 4:
输入: deadends = ["0000"], target = "8888"
输出:-1
提示:
死亡列表 deadends 的长度范围为 [1, 500]。 目标数字 target 不会在 deadends 之中。 每个
deadends 和 target 中的字符串的数字会在 10,000 个可能的情况 ‘0000’ 到 ‘9999’ 中产生。
二、思路详解
其实这道题和迷宫题很像。迷宫题是通路可走,墙壁不能走。而这个开锁的题目是遇到 deadends 里的数字不能拨动,要绕开死亡数字去找到最终的结题答案。然后这种题目我们可以优先想到广度优先算法。其实这道题的结题思路还是比较模板式的。
2.1 题目分析
- 密码锁为 0000 − 9999 可 以 1 0 4 = 10000 种 组 合 即 10000 个 节 点 0000-9999可以10^4=10000种组合即10000个节点 0000−9999可以104=10000种组合即10000个节点
- 时 间 复 杂 度 : O ( 8 × 10000 ) 时间复杂度:O(8\times 10000) 时间复杂度:O(8×10000) 四位密码锁每一位可以往上或往下拨动,及每个节点可以往8个方向扩展。
- 空 间 复 杂 度 : O ( 10000 + d e a d e n d s ) 实 际 上 d e a d e n d s 的 长 度 范 围 为 [ 1 , 500 ] , 长 度 可 忽 略 。 空间复杂度:O(10000+deadends) 实际上deadends的长度范围为[1,500],长度可忽略。 空间复杂度:O(10000+deadends)实际上deadends的长度范围为[1,500],长度可忽略。
遍历过程图解:
2.2 题目技巧点
-
将vector的死亡列表存为Hash_set这样更方便查找。
unordered_set<string> deadends_set(deadends.begin(), deadends.end());
-
字符转整型
‘9’-‘0’=int类型的9 可以参考ASCII码即可弄明白
-
控制拨动范围0-9
常规取余操作
三、代码实现
#include <iostream>
#include <vector>
#include <unordered_set>
#include <queue>
using namespace std;
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
const string start = "0000";
//转成hash_set的原因:如果是用vector去查找当前目标是否在deadends中会慢
//而hash_set就会比较快且方便。
unordered_set<string> deadends_set(deadends.begin(), deadends.end());
//两种特殊情况,如果起始状态直接就在deadends里头和起始状态就是目标值
if (deadends_set.count(start)) return -1; //直接无解返回-1
if (start == target) return 0; //直接解决,返回0
queue<string> que;
//访问过的节点存放到visited当中,hash_set也是为了好访问
unordered_set<string> visited{ start };
int steps = 0;
que.push(start);
while (!que.empty()){
steps++;
//size标记当前循环有多少个节点。后面循环的时候之去扩展这么多节点
//因为在循环过程中que会更新,size会增加
const int size = que.size();
for (int i = 0; i < size; i++) {
const string curr = que.front();
que.pop();
for (int j = 0; j < 4; j++) { //四位密码锁
for (int k = -1; k <= 1; k += 2) { //上下拨 -1和1
string next = curr;
//技巧点:减去字符串0就会变成整型然后加上拨动方向
//+10为了防止出现负数
next[j] = (next[j] - '0' + k + 10) % 10 + '0';
if (next == target)
return steps;
if (deadends_set.count(next) || visited.count(next))
continue;
que.push(next);
visited.insert(next);
}
}
}
}
return -1;
}
};
四、参考链接
https://leetcode-cn.com/explore/learn/card/queue-stack/217/queue-and-bfs/873/
http://zxi.mytechroad.com/blog/searching/leetcode-752-open-the-lock/