核心考点:广度优先搜索 (BFS)、哈希表、字符串
知识点补充:【语法】结构化绑定
题目描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
。每个拨轮可以自由旋转:例如把 '9'
变为 '0'
,'0'
变为 '9'
。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target
代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1
。
【测试用例见文末】
题目详解:
class Solution {
public:int openLock(vector<string>& deadends, string target) {// 如果目标是"0000",直接返回0,因为已经解锁if (target == "0000") {return 0;}// 将死亡数字集合转为哈希表,方便查询unordered_set<string> dead(deadends.begin(), deadends.end());// 如果初始状态 "0000" 就是死亡数字,返回 -1if (dead.count("0000")) {return -1;}// 定义辅助函数,获取当前数字的前一个数字auto num_prev = [](char x) -> char {return (x == '0' ? '9' : x - 1); // '0' -> '9', 其他字符减去 1};// 定义辅助函数,获取当前数字的后一个数字auto num_succ = [](char x) -> char {return (x == '9' ? '0' : x + 1); // '9' -> '0', 其他字符加 1};// 获取当前状态转动一个拨轮后的所有可能状态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;};// 使用队列进行 BFS 搜索,队列中保存当前状态和步数queue<pair<string, int>> q;q.emplace("0000", 0); // 从初始状态开始,步数为 0unordered_set<string> seen = {"0000"}; // 用哈希集合记录已经访问过的状态// BFS 循环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)) {// 如果新状态是目标状态,返回当前步数 + 1if (next_status == target) {return step + 1;}// 否则将新状态加入队列,步数 + 1,并标记为已访问q.emplace(next_status, step + 1);seen.insert(next_status);}}}// 如果队列为空,说明无法到达目标状态return -1;}
};
语法补充:
1.结构化绑定
Structured Binding)它用于从 pair
(或 tuple
)等类型中直接解包多个值。这个语法是在 C++17 引入的。
auto [status, step] = q.front();
相当于,可以简化代码:
pair<string, int> p = q.front(); // 取出队首元素
string status = p.first; // 获取状态
int step = p.second; // 获取步数
2.辅助函数:Lambda函数
具体可看文章:【C++基础】Lambda 函数 基础知识讲解学习及难点解析-CSDN博客
思路分析:
-
题目分析: 这道题目本质上是一个状态转移问题,可以通过广度优先搜索(BFS)来解决。每个锁的状态可以视作一个图的节点,状态之间的转移(旋转拨轮)是图中的边,目标是从初始状态 "0000" 通过最少的操作到达目标状态
target
。同时,要避免经过deadends
中的状态,这些状态会将锁永久锁定。 -
基本思路:
-
如果目标状态是 "0000",直接返回 0,因为已经解锁。
-
使用 BFS,从 "0000" 状态开始进行层级遍历,每次可以选择转动一个拨轮到相邻状态(前进或后退一位)。
-
对于每个状态,检查它是否是
deadends
中的状态,如果是则跳过;如果不是,则加入队列继续搜索。 -
如果到达目标状态
target
,返回当前步数(即最小旋转次数)。 -
如果队列为空,说明无法到达目标状态,返回 -1。
-
-
细节考虑:
-
使用
unordered_set
来存储deadends
和已经访问过的状态,避免重复计算。 -
定义了两个辅助函数
num_prev
和num_succ
用于计算每次旋转的前一个或后一个数字。 -
通过
get
函数生成当前状态的所有可达状态(即转动一个拨轮后可能的状态)。
-
-
时间和空间复杂度:
-
时间复杂度:BFS 搜索的时间复杂度是 O(N),其中 N 是状态空间的大小。由于每个拨轮有 10 个数字,四个拨轮的总状态数为 104=1000010^4 = 10000104=10000,所以时间复杂度是 O(10000)。
-
空间复杂度:BFS 的空间复杂度是 O(N),需要存储队列和已访问的状态,最大空间复杂度为 O(10000)。
-
测试用例:
示例 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]
仅由若干位数字组成