[笔记]BFS——两天做了三道题。。。

BFS——两天做了三道题。。。

好像是涉及图论了,太高深的不懂,记录下做题过程。

BFS简单理解,重点如下几步:

  1. 从起点状态搜到终点状态(队列实现)
  2. 初始化 把起点状态压入列表。
  3. 更新过程:当队列不为空时,每次从队首取一个元素作为这次搜索的起点,当前状态下的每次搜索得到的新合法状态压入队尾,用于下次更新。
  4. 题目里有死路的情况,死路就是非法状态,直接跳过。那如果遇到以前重复过的状态呢?那也是非法状态,因为已经重复过的路不会再搜到新的合法状态,所以也略过。(略过是用哈希表实现的,每次在哈希表里查一下是否曾经存在过,又快又好)
  5. 搜索到某每个状态都更新搜索到当前状态的步数(也是在哈希表里统计,优点是查的快,而且效果好)
  6. 所以哈希表是以每个合法的状态进行搜索,并记录来到这个状态所消耗的步数 unordered_map<state,step>
  7. 一直更新迭代,直到找到终点为止,返回走到终点消耗步数;或者所有合法状态都搜索完(队列空了都没找到,死局return -1)

有一个通俗的说法是 BFS是绘制一颗树的过程,每个节点都是一个状态,每个节点的子节点是从这个状态中搜索来的下一跳状态。

用队列来实现,每次新得到的状态放在队尾,只有当同一批兄弟节点全部出队后才轮得到后进队的孩子节点。挺形象的。

打开转盘锁:LC752

LC:752:——打开转盘锁

比较朴素的一种解法,直接用广度遍历搜索BFS。

//状态的搜索
//仿写的一个BFS,要多注意细节
class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        //八叉树BFS
        string startstr = "0000";
        if(target == startstr) return 0;
        if(find(deadends.begin(),deadends.end(),"0000") != deadends.end()) return -1;
        //queue是队列,先进先出,包含push()与emplace()方法
        queue<string> q;
        set<string> deadlist;
        for(auto d:deadends){
            deadlist.insert(d);
        }
        set<string> visited;
        //push()先构造再压入,emplace()直接在尾部生成
        q.emplace(startstr);
        visited.insert(startstr);
        int level = 0;
        while(!q.empty()){
            int size = q.size();
            while(size-->0){
                string st = q.front();
                q.pop();
                for(int i=0;i<4;i++){
                    char cht = st[i];
                    //注意把int转string 用to_string()
                    string stradd = st.substr(0,i) + to_string(cht == '9' ? 0 : cht - '0' + 1) + st.substr(i+1);
                    string strsub = st.substr(0,i) + to_string(cht == '0' ? 9 : cht - '0' - 1) + st.substr(i+1);
                    //如果找到了,直接返回层数,即搜索次数
                    if(st == target) return level;
                    //如果没找到,当前答案既不能属于deadend,也不能是曾经访问过的数
                    if(!visited.count(stradd) && !deadlist.count(stradd)){
                        q.emplace(stradd);
                        visited.insert(stradd);
                    }
                    if(!visited.count(strsub) && !deadlist.count(strsub)){
                        q.emplace(strsub);
                        visited.insert(strsub);
                    }
                }
            }
            level++;
        }
        return -1;
    }
};

另一种效率更高,也同样方便理解的方法是双向BFS

本题本质上是从一个起始状态 搜到结束状态。这个结束状态是已知的,而且在正向搜索的过程中没有什么跳步,没有什么复杂的约束规则。那么在单向BFS的基础上,同时从终点开始搜索,两个过程都是不断的试探,直到某个状态在正向搜索的过程中遇到了,逆向搜索的过程中也遇到了,就说明一定存在一个经过当前状态的路径,使得从起始状态演化到最终状态成为可能。

好比是从上往下画树,同时,也从下往上画一棵树,当两棵树存在共同的节点,那么两棵树的起点一定就也是相连的。

class Solution {
public:
    string s,t;
    unordered_set<string> dd;
    int openLock(vector<string>& deadends, string target) {
        s = "0000";
        t = target;
        if(s == t) return 0;
        for(auto d:deadends){
            dd.insert(d);
        }
        if(dd.count(s)) return -1;
        int ans = dbfs();
        return ans;
    }

    int dbfs(){
        //双向BFS,那就是两边同步进行,因此有两个队列,两个记录经过状态的哈希表
        queue<string> q1,q2;
        unordered_map<string,int> m1,m2;
        q1.push(s);
        m1[s] = 0;
        q2.push(t);
        m2[t] = 0;
        //当正向、逆向任意一方搜干净的时候都没有结果,那就是死路
        while(!q1.empty() && !q2.empty()){
            int t = -1;
            if(q1.size() <= q2.size()) t = update(q1,m1,m2);
            else t = update(q2,m2,m1);
            if(t!=-1) return t;
        }
    return -1;
    }

    int update(queue<string>& q,unordered_map<string,int>& current,unordered_map<string,int>& other){
        string temp = q.front();
        int cstep = current[temp];
        q.pop();
        for(int i=0;i<4;i++){
            //有每一位数有+1 -1两个转移路线,用数字j来操作。并略过j==0的情况
            //我咋就学不会?
            for(int j= -1;j<2;j++){
                string bak = temp;
                if(j==0) continue;
                char ch = bak[i];
                if(ch == '0' && j == -1) ch = '9';
                else if(ch == '9' && j == 1) ch = '0';
                else ch = (ch - '0' + j)+'0';
                //人家写的这个状态转移,那叫一个漂亮!
                // int origin = temp[i] - '0';
                // int tn = (origin + j) % 10;
                // if(tn == -1) tn = 9;
                bak[i] = ch;
                if(dd.count(bak) || current.count(bak)) continue;
                if(other.count(bak)) return cstep + other[bak] + 1;
                else {
                    q.push(bak);
                    current[bak] = cstep + 1;
                }
            }
        }
    return -1;
    }
};

滑动谜题:LC773

LC773——滑动谜题

同理的,也是个状态转移的过程,用BFS实现

注意 这个实现的时候,放入队列的就是一个Node类对象,因为在遍历过程中需要包含的信息比较多,可以理解为这个Node对象就是画的树上的一个节点吧,总之 在状态转移的时候要把所有需要包含的信息一起放入队列中

class Solution {
public:
    class Node{
    public:
        int x;
        int y;
        string str;
        Node(int _x,int _y,string _str){
            x = _x ; y = _y ; str = _str;    
        }
    };

    string s,t;
    int n = 2,m = 3;
    int x,y;
    int slidingPuzzle(vector<vector<int>>& board) {
        s = "";
        t = "123450";
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                s += to_string(board[i][j]);
                if(board[i][j] == 0){
                    x = i;
                    y = j;
                }
            }
        }
        int ans = bfs();
        return ans;
    }
    //转移方向 上、下、左、右
    int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
    int bfs(){
        queue<Node> q;
        unordered_map<string,int> mp;
        Node root(x,y,s);
        q.emplace(root);
        mp[root.str] = 0;
        while(!q.empty()){
            Node nt = q.front();
            q.pop();
            int step = mp[nt.str];
            if(nt.str == t) return step;
            int dx = nt.x;
            int dy = nt.y;
            for(auto di:dir){
                int nx = dx + di[0];
                int ny = dy + di[1];
                if(nx < 0 || ny <0 || nx >=n || ny >= m) continue;
                //更新0的位置,看是怎么换的,上下左右哪个方向。
                string nstr = update(nt.str,dx,dy,nx,ny);
                if(mp.count(nstr)) continue;
                Node next(nx,ny,nstr);
                cout<<nstr<<endl;
                q.push(next);
                mp[nstr] = step+1;
            }
        }
        return -1;
    }
    
    //注意 千万不要用引用的方式传参啊。。会破坏正在尝试探索的原状态
    string update(string str,int dx,int dy,int nx,int ny){
        string ts = str;
        char temp = str[dx*m + dy];
        str[dx*m + dy] = str[nx*m + ny];
        str[nx*m + ny] = temp;
        return str;
    }
};

这个状态转移的过程一定要熟悉啊,每次看到新题,都有新的状态转移方式,像这种某个点在某个区域内上下左右滑动的过程是怎样模拟的,要熟悉啊。

同理,这个题也是起始状态与终止状态都已知,也可以用双向BFS。

class Solution {
public:
    class Node{
    public:
        int x;
        int y;
        string str;
        Node(int _x,int _y,string _str){
            x = _x ; y = _y ; str = _str;    
        }
    };

    string s,t;
    int n = 2,m = 3;
    int x,y;
    int slidingPuzzle(vector<vector<int>>& board) {
        s = "";
        t = "123450";
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                s += to_string(board[i][j]);
                if(board[i][j] == 0){
                    x = i;
                    y = j;
                }
            }
        }
        //注意开局天胡的情况
        if(s == t) return 0;
        int ans = bfs();
        return ans;
    }
    //转移方向 上、下、左、右
    int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
    int bfs(){
        queue<Node> q1,q2;
        unordered_map<string,int> mp1,mp2;
        Node start(x,y,s);
        Node end(1,2,t);
        q1.emplace(start);
        q2.emplace(end);
        mp1[start.str] = 0;
        mp2[end.str] = 0;
        int step;
        //双向BFS就是要两边交替进展,因为每次节点更新都会得到新状态,所以每次取队列元素少的队先遍历。
        while(!q1.empty() && !q2.empty()){
            int t = -1;
            if(q1.size() <= q2.size()){
                t = update(q1,mp1,mp2);
            }
            else {
                t = update(q2,mp2,mp1);
            }
            if(t != -1) return t;
        }
        return -1;
    }
    
    //注意 千万不要用引用的方式传参啊。。会破坏正在尝试探索的原状态
    int update(queue<Node>& q,unordered_map<string,int>& cur,unordered_map<string,int>& other){
        //队列要有进有出,很重要,否则死循环,别犯错了。
        Node nt = q.front();
        q.pop();
        int dx = nt.x;
        int dy = nt.y;
        string origin = nt.str;
        int step = cur[origin];
        for(auto di:dir){
            int nx = dx + di[0];
            int ny = dy + di[1];
            if(nx < 0 || ny <0 || nx >=n || ny >= m) continue;
            //更新0的位置,看是怎么换的,上下左右哪个方向。
            string bak = origin;
            char temp = bak[dx*m + dy];
            bak[dx*m + dy] = bak[nx*m + ny];
            bak[nx*m + ny] = temp;
            if(cur.count(bak)) continue;
            if(other.count(bak)) return step + other[bak] + 1;
            else{
                Node next(nx,ny,bak);
                q.push(next);
                cur[bak] = step+1;
            }
        }
        return -1;
    }
};

蛇梯棋:LC909

LC909——蛇梯棋

这个题有点特殊,我们知道起始状态,也知道终止状态。但是因为有个状态跳变的情况,状态的更新并不是两个方向对等的,我们只能知道如何从起始状态搜索到达终止状态,而没有从终止状态反过来搜的方式(也许真的有?但是我还没想明白,认为是没有的)。所以用简单的BFS就可以了。

Q: 感觉还是BFS,每个节点有6个分支。可能有的节点能够跳跃。

A: 题目原意是once per move 每次移动只许跳一下 不许连跳。中文翻译我还以为是整个过程只允许经过一次虫洞。

Q: 但是真的存在无法到达的情况?

A: 存在的,比如在第29格的可选项是30 31 32 33 34 35,这些格子统统存在虫洞且全部指向29格以前的格子。

有个问题需要处理,就是格子的索引值怎么转换成下标?

一种办法是压缩成一维的,用一维数组来表示。

另一种接近模拟法的思路就是手动转换一下,,锻炼下逻辑能力

//输入版的规模N*N的一边,输入要转化的索引,输出该索引对应在版board中的下标表示。第r行第c列
pair<int,int> turned(int N,int idx){
    int lor = 0;
    int r = N - (idx-1)/N - 1;
    int c;
    if((idx-1)/N & 1) lor = 1;
    if(lor){
        c = N - (idx-1)%N -1;
    }
    else c = (idx-1)%N;
    return {r,c};
}
class Solution {
    //写个函数把索引转换成二维数组的行列。从索引值转化到矩阵下标。
    //当然转成一维会更简单了,权当锻炼一下逻辑吧。。。
    pair<int,int> turned(int N,int idx){
        int lor = 0;
        int r = N - (idx-1)/N - 1;
        int c;
        if((idx-1)/N & 1) lor = 1;
        if(lor){
            c = N - (idx-1)%N -1;
        }
        else c = (idx-1)%N;
        return {r,c};
    }

    int bfs(int N,vector<vector<int>>& board){
        int maxsize = N*N;
        //矩阵方格的索引是从1开始的,索引1对应的搜索步数为0(还没开始)
        queue<int> q;
        q.push(1);
        unordered_map<int,int> mp;
        mp[1] = 0;
        while(!q.empty()){
            int idx = q.front();
            int step = mp[idx];
            q.pop();
            pair<int,int> pos = turned(N,idx);
            int r = pos.first;
            int c = pos.second;
            for(int i=1;i<=6;i++){
                int idbak = idx+i;
                //如果在基准索引idx的基础上选择了介于1-6之间的某个格子到头了,就返回找到idx的step步再加当前一步。
                if(idbak == maxsize) return step+1;
                if(idbak > maxsize) continue;
                pair<int,int> npos = turned(N,idbak);
                int nr = npos.first,nc = npos.second;
                //如果有虫洞直通索引N*N 也是返回step+1
                if(board[nr][nc] == maxsize) return step+1;
                //如果下一步有虫洞但没有直通终点,那么先更新虫洞目的地,再将目的地加入哈希表
                if(board[nr][nc] != -1) {
                    idbak = board[nr][nc];
                }
                //如果走到了已经出现过的索引说明这一步是无效的,跳过。
                if(mp.count(idbak)) continue;
                //如果这一步走过来没有虫洞,那么直接将当前索引加入队列下次遍历
                //如果这一步经过了虫洞,那么记得加入队列的是冲动目的地的索引,别搞错了。。
                q.push(idbak);
                mp[idbak] = step+1;

            }
        }
        return -1;
    }

public:
    int snakesAndLadders(vector<vector<int>>& board) {
        int N = board.size();
        //bfs 容器以idx即方格编号为索引查找
        int ans = bfs(N,board);
        return ans;

    }
};

心得

BFS的话,模板是差不多的,我认为的难点是状态转移的这个模拟过程,很不熟练,,这个怎么说,感觉做一个题就看到一个新方法,多做多看多学把。。。
两天才看三道题,人要裂开了。。。😅

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值