题目链接:开密码锁
题目:
一个密码锁由 4 个环形转轮组成,每个转轮由 0 ~ 9 这 10 个数字组成。每次可以上下拨动一个转轮,如可以将一个转轮从 0 拨到 1,也可以从 0 拨到 9。密码锁有若干死锁状态,一旦 4 个转轮被拨到某个死锁状态,这个锁就不可能打开。密码锁的状态可以用一个长度为 4 的字符串表示,字符串中的每个字符对应某个转轮上的数字。输入密码锁的密码和它的所有死锁状态,请问至少需要拨动转轮多少次才能从起始状态 "0000" 开始打开这个密码锁?如果锁不可能打开,则返回 -1。
例如,如果某个密码锁的密码是 "0202",它的死锁状态列表是 ["0102", "0201"],那么至少需要拨动转轮 6 次才能打开这个密码锁,一个可行的开锁状态序列是 "0000"->"1000"->"1100"->"1200"->"1201"->"1202"->"0202"。虽然序列 "0000"->"0100"->"0200"->"0201"->"0202" 更短,只需要拨动 4 次转轮,但它包含死锁状态 "0201",因此这是一个无效的开锁序列。
分析:
密码锁 4 个转轮上的数字定义了密码锁的状态,转动密码锁的转轮可以改变密码锁的状态。一般而言,如果一个问题是关于某事物状态的改变,那么可以考虑把问题转换成图搜索的问题。事物的每个状态是图中的一个节点,如果一个状态能够转变到另一个状态,那么这两个状态对应的节点之间有一条边相连。
对于这个问题而言,密码锁的每个状态都对应着图中的一个节点,如状态 "0000" 是一个节点,"0001" 是另一个节点。如果转动某个转轮一次可以让密码锁从一个状态转移到另一个状态,那么这两个状态之间有一条边相连。例如,将状态 "0000" 分别向上或向下转动 4 个转轮中的一个,可以得到 8 个状态,即 "0001"、"0009"、"0010"、"0090"、"0100"、"0900"、"1000" 和 "9000",那么图中节点 "0000" 就有 8 条边分别和这 8 个状态对应的节点相连。
由于题目要求的是找出节点 "0000" 到密码的对应节点的最短路径,因此应该采用广度优先搜索。这是因为广度优先搜索是从起始节点开始首先到达所有距离为 1 的节点,接着到达所有距离为 2 的节点。广度优先搜索一定是从起始节点沿着最短路径到达目标节点的。
搜索密码锁对应的图时还要注意避开死锁状态对应的节点,因为一旦到达这些节点之后就不能继续向下搜索。
代码实现:
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
unordered_set<string> dead(deadends.begin(), deadends.end());
if (dead.count("0000") || dead.count(target))
return -1;
unordered_set<string> visited;
queue<string> q1, q2;
q1.push("0000");
int step = 0;
while (!q1.empty())
{
string cur = q1.front();
q1.pop();
if (cur == target)
return step;
vector<string> neighbors = getNeighbors(cur);
for (string& neighbor : neighbors)
{
if (!dead.count(neighbor) && !visited.count(neighbor))
{
q2.push(neighbor);
visited.insert(neighbor);
}
}
if (q1.empty())
{
q1 = q2;
q2 = queue<string>();
++step;
}
}
return -1;
}
private:
vector<string> getNeighbors(string& cur) {
vector<string> neighbors;
for (int i = 0; i < cur.size(); ++i)
{
char old = cur[i];
cur[i] = old == '0' ? '9' : old - 1;
neighbors.push_back(cur);
cur[i] = old == '9' ? '0' : old + 1;
neighbors.push_back(cur);
cur[i] = old;
}
return neighbors;
}
};
上述代码用两个队列实现广度优先搜索。队列 q1 中存放的是需要转动 n 次到达的节点,队列 q2 中存放的是和队列 q1 中的节点相连但是还没有搜索到的节点。当队列 q1 中的节点都删除之后,接着遍历需要转动 n + 1 次到达的节点,也就是队列 q2 中的节点,此时变量 step 加 1。
如果仔细比较可以发现上述函数 openLock 和面试题 108 中实现单向广度优先搜索的代码非常类似,实际上,用广度优先搜索解决大多数最短路径问题的代码都大同小异。因此,应聘者应该熟练掌握这个代码模板,这样在面试的时候如果遇到类似的问题就能很快写出正确的代码。
和面试题 108 类似,也可以用双向广度优先搜索来解决这个问题。