《剑指 Offer》专项突破版 - 面试题 109 : 开密码锁(C++ 实现)

题目链接开密码锁

题目

一个密码锁由 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 类似,也可以用双向广度优先搜索来解决这个问题。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值