LeetCode: 752. 打开转盘锁(双向bfs)

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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;
    }
};

参考:https://leetcode-cn.com/problems/open-the-lock/solution/gong-shui-san-xie-yi-ti-shuang-jie-shuan-wyr9/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
给定一个整数数组 nums 和一个目标值 target,要求在数组中找出两个数的和等于目标值,并返回这两个数的索引。 思路1:暴力法 最简单的思路是使用两层循环遍历数组的所有组合,判断两个数的和是否等于目标值。如果等于目标值,则返回这两个数的索引。 此方法的时间复杂度为O(n^2),空间复杂度为O(1)。 思路2:哈希表 为了优化时间复杂度,可以使用哈希表来存储数组中的元素和对应的索引。遍历数组,对于每个元素nums[i],我们可以通过计算target - nums[i]的值,查找哈希表中是否存在这个差值。 如果存在,则说明找到了两个数的和等于目标值,返回它们的索引。如果不存在,将当前元素nums[i]和它的索引存入哈希表中。 此方法的时间复杂度为O(n),空间复杂度为O(n)。 思路3:双指针 如果数组已经排序,可以使用双指针的方法来求解。假设数组从小到大排序,定义左指针left指向数组的第一个元素,右指针right指向数组的最后一个元素。 如果当前两个指针指向的数的和等于目标值,则返回它们的索引。如果和小于目标值,则将左指针右移一位,使得和增大;如果和大于目标值,则将右指针左移一位,使得和减小。 继续移动指针,直到找到两个数的和等于目标值或者左指针超过了右指针。 此方法的时间复杂度为O(nlogn),空间复杂度为O(1)。 以上三种方法都可以解决问题,选择合适的方法取决于具体的应用场景和要求。如果数组规模较小并且不需要考虑额外的空间使用,则暴力法是最简单的方法。如果数组较大或者需要优化时间复杂度,则哈希表或双指针方法更合适。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值