你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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
提示:
1 <= deadends.length <= 500
deadends[i].length == 4
target.length == 4
target 不在 deadends 之中
target 和 deadends[i] 仅由若干位数字组成
分析:
因为求的是最小旋转次数,容易想到BFS,思路并不难,重点是问题的转化,要同时记录当前字符串和次数,利用哈希表优化。
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
if (target == "0000") {
return 0;
}
unordered_set<string> dead(deadends.begin(), deadends.end());
if (dead.count("0000")) {
return -1;
}
auto num_prev = [](char x) -> char {
return (x == '0' ? '9' : x - 1);
};
auto num_succ = [](char x) -> char {
return (x == '9' ? '0' : x + 1);
};
// 枚举 status 通过一次旋转得到的数字
auto get = [&](string& status) -> vector<string> {
vector<string> ret;
for (int i = 0; i < 4; ++i) {
char num = status[i];
status[i] = num_prev(num);
ret.push_back(status);
status[i] = num_succ(num);
ret.push_back(status);
status[i] = num;
}
return ret;
};
queue<pair<string, int>> q;
q.emplace("0000", 0);
unordered_set<string> seen = {"0000"};
while (!q.empty()) {
auto [status, step] = q.front();
q.pop();
for (auto&& next_status: get(status)) {
if (!seen.count(next_status) && !dead.count(next_status)) {
if (next_status == target) {
return step + 1;
}
q.emplace(next_status, step + 1);
seen.insert(move(next_status));
}
}
}
return -1;
}
};
这里重点要学习的是双向BFS的方法,如题解中图片所示,搜索空间的最大宽度会对空间产生极大的影响,所以提出了双向BFS的方法。即从两个方向开始搜索,一旦搜索到相同的值,那就说明找到了一条联通起点和终点的最短路径。至于为什么是最短,应该也好理解,因为我们是广度优先,本来就是基于目前最少的步数进行的搜索。
基本思路:
1.创建两个队列,分别用于两个方向的搜索。
2.创建两个哈希表,用于解决相同节点重复搜索的问题,同时记录转换次数(也就是步数)。
3.为了尽可能使两个搜索方向的深度平均,以达到我们减少最大宽度的目的(一般来说,深度越深,宽度越大,我们尽量避免某一方向深度过高),每次从队列中取值扩展队列时,先判断哪个队列容量较少(反之,容量较少往往意味着深度越小)。
4.如果在搜索过程中找到了对方搜索过的节点,则说明找到了最短路径。
模板如下:
d1、d2 为两个方向的队列
m1、m2 为两个方向的哈希表,记录每个节点距离起点的距离
// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
while(!d1.empty() && !d2.empty()) {
if (d1.size() <= d2.size()) {
update(d1, m1, m2);
} else {
update(d2, m2, m1);
}
}
// update 为从队列 d 中取出一个元素进行「一次完整扩展」的逻辑
void update(queue d, unordered_map cur, unordered_map other) {}
完整代码如下:
class Solution {
public:
string s, t;
unordered_set<string> st;
int openLock(vector<string>& deadends, string target) {
s = "0000";
t = target;
if(s == t) return 0;
for(const auto &d : deadends) st.insert(d);
if(st.count(s)) return -1;
int ans = bfs();
return ans;
}
int bfs(){
queue<string> d1, d2;
unordered_map<string, int> m1, m2;
d1.push(s); m1[s] = 0;
d2.push(t); m2[t] = 0;
while(d1.size() && d2.size()){
int t = -1;
if(d1.size() <= d2.size()){
t = update(d1, m1, m2);
}else{
t = update(d2, m2, m1);
}
if(t != -1) return t;
}
return -1;
}
int update(queue<string> &q, unordered_map<string, int> &cur, unordered_map<string, int> &other){
string t = q.front(); q.pop();
int step = cur[t];
for(int i = 0; i < 4; i++){
for(int j = -1; j <= 1; j++){
if(j == 0) continue;
int origin = t[i] - '0';
int next = (origin + j) % 10;
if(next == -1) next = 9;
string copy = t;
copy[i] = '0' + next;
if(st.count(copy) || cur.count(copy)) continue;
if(other.count(copy)) return step + 1 + other[copy];
else{
q.push(copy);
cur[copy] = step + 1;
}
}
}
return -1;
}
};