BFS练习

关于BFS的入门和练习题的一些题解 (题目节选自b站一只会code的小金鱼)

PS:代码只贴了solve()部分,其余重要的宏定义有 #define rep(i,a,b) for(int i=(a);i<=(b);i++) #define endl '\n' #define int long long #define pii pair<int,int>

P1746 离开中山路

现在爱与愁大神在 x1,y1 处,车站在 x2,y2 处。现在给出一个 n×n(n≤1000) 的地图,0 表示马路,1 表示店铺(不能从店铺穿过),爱与愁大神只能垂直或水平着在马路上行进。爱与愁大神为了节省时间,他要求最短到达目的地距离(每两个相邻坐标间距离为 1)。你能帮他解决吗?

void solve(){
    int n;cin>>n;
    vector<string> s(n+1);
    rep(i,1,n)cin>>s[i];
    rep(i,1,n)s[i]=' '+s[i];
    int a,b,c,d;cin>>a>>b>>c>>d;

    vector<vector<int>> vis(n+1,vector<int>(n+1,-1));//不要再用memset赋值了

    auto inmap=[&](int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=n)return 1;
        return 0;
    };
    int dx[]={1,-1,0,0};
    int dy[]={0,0,1,-1};

    auto bfs=[&]()->int{
        queue<pair<int,int>> q; //建队列
        q.push({a,b});          // push 起点
        vis[a][b]=0;            // 起点标记

        while(q.size()){
            auto t=q.front();   // 取队首
            q.pop();            // pop队首
            rep(i,0,3){
                int x=t.first+dx[i];
                int y= t.second+dy[i];
                if(inmap(x,y)&&s[x][y]!='1'&&vis[x][y]==-1){
                    q.push({x,y}); //这个别忘了
                    vis[x][y]=vis[t.first][t.second]+1;
                    // 因为是一层一层的就像涟漪(权值为1), 
                    // 所以第几层遍历到的 与几点距离就是层数
                }
                if(vis[c][d]>0)return vis[c][d];
            }
        }
        return -1;
    };
    bfs();
    cout<<vis[c][d]<<endl;   
}

P1443 马的遍历

有一个 n×m 的棋盘,在某个点 (x,y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步 (马走日)。(不能到达则输出 −1)。

void solve(){
    int n,m,a,b;cin>>n>>m>>a>>b;
    
    vector<vector<int>> vis(n+1,vector<int>(m+1,-1));

    auto inmap=[&](int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=m)return 1;
        return 0;
    };
    int dx[]={2,2,-2,-2,1,1,-1,-1};
    int dy[]={1,-1,1,-1,2,-2,2,-2};
    auto bfs=[&](){
        queue<pair<int,int>> q;
        q.push({a,b});
        vis[a][b]=0;
        
        while(q.size()){
            auto t=q.front();
            q.pop();
            rep(i,0,7){ // 0~7
                int x=t.first+dx[i];
                int y=t.second+dy[i];
                if(inmap(x,y)&& vis[x][y]==-1){
                    q.push({x,y});//
                    vis[x][y]=vis[t.first][t.second]+1;    
                }
            }
        }
    };
    bfs();
    rep(i,1,n){
        rep(j,1,m){
            cout<<vis[i][j]<<" ";
        }cout<<endl;
    }
}

多源BFS

( 其实没什么差别 , 在while之前把起点都加入并初始化即可)

P1332 血色先锋队

题目描述

军团是一个 nm 列的矩阵,每个单元是一个血色先锋军的成员。感染瘟疫的人,每过一个小时,就会向四周扩散瘟疫,直到所有人全部感染上瘟疫。你已经掌握了感染源的位置,任务是算出血色先锋军的领主们感染瘟疫的时间,并且将它报告给巫妖王,以便对血色先锋军进行一轮有针对性的围剿。

输入格式

第 1 行:四个整数 nmab,表示军团矩阵有 nm 列。有 a 个感染源,b 为血色敢死队中领主的数量。

接下来 a 行:每行有两个整数 xy,表示感染源在第 x 行第 y 列。

接下来 b 行:每行有两个整数 xy,表示领主的位置在第 x 行第 y 列。

输出格式

第 1 至 b 行:每行一个整数,表示这个领主感染瘟疫的时间,输出顺序与输入顺序一致。如果某个人的位置在感染源,那么他感染瘟疫的时间为 0。

输入输出样例

**输入 **

5 4 2 3
1 1
5 4
3 3
5 3
2 4

输出

3
1
3

说明/提示

输入输出样例 1 解释

如下图,标记出了所有人感染瘟疫的时间以及感染源和领主的位置。

img

数据规模与约定

对于 100% 的数据,保证 1≤n,m≤500,1≤a,b≤105。

void solve(){
    int n,m,a,b;cin>>n>>m>>a>>b;
    vector<pair<int,int>> sor;
    vector<pair<int,int>> tar;
    // map<pair<int,int>,int> mp;直接用二维数组记录最小时间即可
    vector<vector<int>> vis(n+1,vector<int>(m+1,-1));

    rep(i,1,a){
        int x,y;cin>>x>>y;
        sor.push_back({x,y});
    }    
    rep(i,1,b){
        int x,y;cin>>x>>y;
        tar.push_back({x,y});
    }

    auto inmap=[&](int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=m)return 1;
        return 0;
    };

    int dx[]={1,-1,0,0};
    int dy[]={0,0,1,-1};

    auto bfs=[&](){
        queue<pair<int,int>> q;
        for(auto &i:sor){//
            q.push(i);
            vis[i.first][i.second]=0;//
        }
        while(q.size()){
            auto t=q.front();
            q.pop();
            rep(i,0,3){
                int x=t.first+dx[i];
                int y=t.second+dy[i];
                if(inmap(x,y) && vis[x][y]==-1){
                    q.push({x,y});
                    vis[x][y]=vis[t.first][t.second]+1;//是t不是p
                }
            }
        }
    };
    bfs();
    for(auto &[x,y]:tar){
        cout<<vis[x][y]<<endl;
    }
    
}

P1162 填涂颜色

题目描述

由数字 0 组成的方阵中,有一任意形状的由数字 1 构成的闭合圈。现要求把闭合圈内的所有空间都填写成 2。例如:6×6 的方阵(n=6),涂色前和涂色后的方阵如下:

如果从某个 0 出发,只向上下左右 4 个方向移动且仅经过其他 0 的情况下,无法到达方阵的边界,就认为这个 0 在闭合圈内。闭合圈不一定是环形的,可以是任意形状,但保证闭合圈内的 0 是连通的(两两之间可以相互到达)。

0 0 0 0 0 0
0 0 0 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 1 0 1
1 1 1 1 1 1
0 0 0 0 0 0
0 0 0 1 1 1
0 1 1 2 2 1
1 1 2 2 2 1
1 2 2 1 2 1
1 1 1 1 1 1

输入格式

每组测试数据第一行一个整数 n(1≤n≤30)。

接下来 n 行,由 0 和 1 组成的 n×n 的方阵。

方阵内只有一个闭合圈,圈内至少有一个 0。

输出格式

已经填好数字 2 的完整方阵。

输入输出样例

输入 #1

6
0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 0 0 1
1 1 1 1 1 1

输出 #1

0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 2 2 1
1 1 2 2 2 1
1 2 2 2 2 1
1 1 1 1 1 1

说明/提示

对于 100% 的数据,1≤n≤30

void solve(){
    int n;cin>>n;
    vector<vector<int>> s(n+2,vector<int>(n+2));//在外面加一圈0 把外围的0全部联通起来方便搜索,这简直太妙了
    rep(i,1,n){
        rep(j,1,n){
            cin>>s[i][j];
        }
    }
    auto inmap=[&](int x,int y){
        if(x>=0&&x<=n+1&&y>=0&&y<=n+1)return 1;
        return 0;
    };
    int dx[]={1,-1,0,0  ,1,1,-1,-1};
    int dy[]={0,0,1,-1  ,1 ,-1,1,-1};
    int x1,y1;
    vector<vector<int>> vis(n+2,vector<int>(n+2));
    auto bfs=[&](int u,int v){
        queue<pair<int,int>> q;
        q.push({u,v});
        vis[u][v]=1;

        while(q.size()){
            auto t=q.front();
            q.pop();
            rep(i,0,3){
                int x=t.first+dx[i];
                int y = t.second+dy[i];
                if(inmap(x,y)&&vis[x][y]==0&&s[x][y]==0){
                    vis[x][y]=1;
                    q.push({x,y});
                }
                if(inmap(x,y) && s[x][y]==1){
                    x1=x;y1=y;
                }
            }
        }
    };
    bfs(0,0);
    auto dfs=[&](auto self,int x,int y)->void{
        rep(i,0,7){
            int a=x+dx[i];
            int b=y+dy[i];
            if(inmap(a,b)&&s[a][b]==1&&vis[a][b]==0){
                vis[a][b]=1;
                self(self,a,b);
            }
        }
    };
    dfs(dfs,x1,y1);// 其实用bfs也行
    rep(i,1,n){
        rep(j,1,n){
            if(vis[i][j]==0){
                s[i][j]=2;
            }
            cout<<s[i][j]<<' ';
        }cout<<endl;
    }

}

P2895 [USACO08FEB] Meteor Shower S

题目描述

贝茜听说一场特别的流星雨即将到来:这些流星会撞向地球,并摧毁它们所撞击的任何东西。她为自己的安全感到焦虑,发誓要找到一个安全的地方(一个永远不会被流星摧毁的地方)。

如果将牧场放入一个直角坐标系中,贝茜现在的位置是原点,并且,贝茜不能踏上一块被流星砸过的土地。

根据预报,一共有 M 颗流星 (1≤M≤50,000) 会坠落在农场上,其中第 i 颗流星会在时刻 T**i(0≤T**i≤1000)砸在坐标为 (X**i,Y**i)(0≤X**i≤300,0≤Y**i≤300) 的格子里。流星的力量会将它所在的格子,以及周围 4 个相邻的格子都化为焦土,当然贝茜也无法再在这些格子上行走。

贝茜在时刻 0 开始行动,她只能在会在横纵坐标 X,Y≥0 的区域中,平行于坐标轴行动,每 1 个时刻中,她能移动到相邻的(一般是 4 个)格子中的任意一个,当然目标格子要没有被烧焦才行。如果一个格子在时刻 t 被流星撞击或烧焦,那么贝茜只能在 t 之前的时刻在这个格子里出现。 贝茜一开始在 (0,0)。

请你计算一下,贝茜最少需要多少时间才能到达一个安全的格子。如果不可能到达输出 −1。

输入格式

M+1 行,第 1 行输入一个整数 M,接下来的 M 行每行输入三个整数分别为 X**i,Y**i,T**i

输出格式

贝茜到达安全地点所需的最短时间,如果不可能,则为 −1。

输入输出样例

输入 #1

4
0 0 2
2 1 2
1 1 2
0 3 5

输出 #1

5

坐标不能低于0,但可以超300! 恶心

void solve(){
    int n;cin>>n;
    struct file{
        int x,y,t;
    };
    vector<file> v;
    rep(i,1,n){
        int x,y,t;cin>>x>>y>>t;
        v.push_back({x,y,t});
    }
    //记录最早的不能走的时间,记录所有安全格子
    auto inmap=[&](int x,int y){
        if(x>=0&&x<=304&&y>=0&&y<=304)return 1;
        return 0;
    };
    int dx[]={0,0,1,-1};
    int dy[]={-1,1,0,0};

    //vector<vector<pair<int,int>>> vis(305,vector<pair<int,int>>(305,{0,1e18}));
    // 不要一个数组实现两个功能吧 ,其实一个就够了
     vector<vector<int>> vis(305,vector<int>(305,1e9));
    for(auto &[x,y,t]:v){
        vis[x][y]=min(vis[x][y],t);
        rep(i,0,3){
            int u=x+dx[i];
            int v=y+dy[i];
            if(inmap(u,v)){
                vis[u][v]=min(vis[u][v],t);
            }
        }
    };
    int ans=1e9;
    if(vis[0][0]==0){
        ans=-1;cout<<ans<<endl;return;
    }
    auto bfs=[&](){
        queue<pair<int,int>> q;
        q.push({0,0});
        vector<vector<int>> time(305,vector<int>(305,-1));
        time[0][0]=0;

        while(q.size()){
            auto t=q.front();
            q.pop();

            rep(i,0,3){
                int x=t.first+dx[i];
                int y=t.second+dy[i];
                if(inmap(x,y) && time[x][y]==-1 && time[t.first][t.second]+1<vis[x][y]){
                    q.push({x,y});
                    time[x][y]=time[t.first][t.second]+1;
                }
                if(inmap(x,y)&&vis[x][y]==1e9){
                    ans=min(ans,time[x][y]);
                }
            }

        }
    };
    bfs();
    if(ans==1e9)ans=-1;
    cout<<ans<<endl;

}

P2658 汽车拉力比赛

题目描述

博艾市将要举行一场汽车拉力比赛。

赛场凹凸不平,所以被描述为 NM 的网格来表示海拔高度 (1≤M,N≤500),每个单元格的海拔范围在 0 到 109 之间。

其中一些单元格被定义为路标。组织者希望给整个路线指定一个难度系数 D,这样参赛选手从任一路标到达别的路标所经过的路径上相邻单元格的海拔高度差不会大于 D 。也就是说这个难度系数 D 指的是保证所有路标相互可达的最小值。任一单元格和其东西南北四个方向上的单元格都是相邻的。

输入格式

第 1 行两个整数 MN。第 2 行到第 M+1 行,每行 N 个整数描述海拔高度。第 2+M 行到第 1+2M

行,每行 N 个整数,每个数非 0 即 1,1 表示该单元格是一个路标。

输出格式

一个整数,即赛道的难度系数 D

输入输出样例

输入 #1

3 5 
20 21 18 99 5  
19 22 20 16 26
18 17 40 60 80
1 0 0 0 1
0 0 0 0 0
0 0 0 0 1

输出 #1

21

就是在BFS前加一步 二分 高度差, 注意是高度差而不是高度

void solve(){
    int n ,m;cin>>n>>m;
    vector<vector<int>> s(n+1,vector<int>(m+1));
    rep(i,1,n){
        rep(j,1,m){
            cin>>s[i][j];
        }
    }
    vector<pii> tar;
    rep(i,1,n){
        rep(j,1,m){
            int x;cin>>x;
            if(x==1){
                tar.push_back({i,j});
            }
        }
    }

    int dx[]={0,0,1,-1};
    int dy[]={1,-1,0,0};

    auto inmap=[&](int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=m)return 1;
        return 0;
    };

    if(tar.size()<=1){
        cout<<0<<endl;return;
    }

    int l=-1,r=1e9+1;
    while(l+1!=r){
        int mid=l+r>>1;
        vector<vector<int>> vis(n+1,vector<int>(m+1,-1));
        auto bfs=[&](int u,int v,int d){
            queue<pii> q;
            q.push({u,v});
            vis[u][v]=1;

            while(q.size()){
                auto t=q.front();
                q.pop();
                rep(i,0,3){
                    int x = t.first+dx[i];
                    int y = t.second+dy[i];
                    if( inmap(x,y) && vis[x][y]==-1 && abs(s[x][y]-s[t.first][t.second])<=d ){ 
                        q.push({x,y});
                        vis[x][y]=1;
                    }
                }
            }
        };
        bfs(tar[0].first, tar[0].second, mid);
        int ok=1;
        for(auto &[x,y]:tar){
            if(vis[x][y]!=1){
                ok=0;break;
            }
        }
        if(ok)r=mid; // 不是l=mid , 之前写错了, 闹麻了, 一度怀疑自己一直以来用的二分板子出问题
        else l=mid;
    }
    cout<<r<<endl;
    
}

双端队列 BFS(Deque BFS)

双端队列 BFS(Deque BFS)是一种针对边权仅为 0 或 1 的图的单源最短路径算法,是广度优先搜索(BFS)的优化变体。它通过双端队列(deque)高效维护节点的访问顺序,解决了普通 BFS 在带 0 权边图中无法直接求最短路径的问题,同时比 Dijkstra 算法更高效。

一、核心应用场景

双端队列 BFS 的适用条件非常明确:

  • 图中所有边的权重只能是 0 或 1(允许存在混合的 0 权边和 1 权边);
  • 求解单源最短路径(从一个起点到其他所有节点的最短距离)。

二、为什么需要双端队列 BFS?

普通 BFS 仅适用于边权为 1 的无权图(或视为权值全为 1),此时最短路径可通过 “层次遍历” 直接得到(每一层对应距离 + 1)。但当图中存在 0 权边时,普通 BFS 会失效:

  • 0 权边连接的节点可能与当前节点处于 “同一层次”(距离相同),甚至距离更短,需要优先处理;
  • 若仍按普通 BFS 的 “队尾插入” 规则,会导致节点距离计算错误。

三、算法原理

双端队列 BFS 的核心是利用双端队列(deque)的 “两端插入 / 删除” 特性,动态维护节点的访问顺序,确保队列始终保持 “距离非递减” 的顺序:队头节点的距离 ≤ 队列中所有节点的距离 ≤ 队尾节点的距离。因此,当节点被从队列中取出时,其最短距离已最终确定(不会被后续节点更新),确保了结果的正确性。

核心操作规则

  • 维护一个距离数组dist[]dist[u]表示起点到节点u的最短距离(初始化为无穷大,起点距离设为 0);

  • 使用双端队列存储待处理节点,起点先入队;

  • 当处理节点 u时,遍历其所有邻居 v

    • 若边u→v的权重为0:则v的最短距离可能与u相同(dist[v] = dist[u]),此时将v插入队头(优先处理,保证同距离节点先被访问);
    • 若边u→v的权重为1:则v的最短距离为u的距离 + 1(dist[v] = dist[u] + 1),此时将v插入队尾(同普通 BFS,作为下一层节点);
  • 每个节点仅需处理一次(当从队列中取出时,其最短距离已确定,无需重复处理)。

算法 适用场景 数据结构 时间复杂度 优势
普通 BFS 边权全为 1 的图 队列(queue) O(N + M) 简单,适用于纯 1 权边图
Dijkstra 非负权图(含 0/1 权边) 优先队列(堆) O(M + N log N) 适用范围广(任意非负权)
双端队列 BFS 边权仅为 0 或 1 的图 双端队列(deque) O(N + M) 比 Dijkstra 更高效(0/1 权场景)

P4554 小明的游戏

题目描述

小明最近喜欢玩一个游戏。给定一个 n×m 的棋盘,上面有两种格子 #@。游戏的规则很简单:给定一个起始位置和一个目标位置,小明每一步能向上,下,左,右四个方向移动一格。如果移动到同一类型的格子,则费用是 0,否则费用是 1。请编程计算从起始位置移动到目标位置的最小花费。

输入格式

输入文件有多组数据。
输入第一行包含两个整数 nm,分别表示棋盘的行数和列数。
输入接下来的 n 行,每一行有 m 个格子(使用 # 或者 @ 表示)。
输入接下来一行有四个整数 x1,y1,x2,y2,分别为起始位置和目标位置。
当输入 nm 均为 0 时,表示输入结束。1≤n,m≤500。

输出格式

对于每组数据,输出从起始位置到目标位置的最小花费。每一组数据独占一行。

输入输出样例

输入 #1

2 2
@#
#@
0 0 1 1
2 2
@@
@#
0 1 1 0
0 0

输出 #1

2
0
void solve(){
    int n,m;
    while(cin>>n>>m && (n != 0 || m != 0)){  // 修改循环条件
        vector<string> s(n+1);
        rep(i,1,n) cin>>s[i];
        rep(i,1,n) s[i]=' '+s[i];  // 使坐标从1开始
        
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        x1++; y1++; x2++; y2++;  // 转换为1-based坐标
        
        int dx[]={0,0,-1,1};
        int dy[]={1,-1,0,0};
        
        auto inmap=[&](int x,int y){
            return x>=1 && x<=n && y>=1 && y<=m;
        };
        
        vector<vector<int>> vis(n+1, vector<int>(m+1, -1));
        
        auto bfs=[&](){
            deque<pii> dq;
            dq.push_front({x1,y1});
            vis[x1][y1]=0;
            
            while(!dq.empty()){
                auto [u,v]=dq.front(); // 千万不能用auto && [u,v], 这太细节了, 超时都是它导致的
                dq.pop_front();
                
                // 提前退出优化
                if(u == x2 && v == y2) return;
                
                rep(i,0,3){
                    int x=u+dx[i];
                    int y=v+dy[i];
                    if(inmap(x,y) && vis[x][y]==-1){
                        if(s[x][y]==s[u][v]){
                            dq.push_front({x,y});
                            vis[x][y]=vis[u][v];
                        }else{
                            dq.push_back({x,y});
                            vis[x][y]=vis[u][v]+1;
                        }
                    }
                }
            }
        };
        
        bfs();
        cout<<vis[x2][y2]<<endl;
    }
}

不能用auto && [u,v] 的原因

当你使用引用 (&) 绑定队列前端元素时,在处理过程中如果队列发生变化(例如通过 push_frontpush_back 添加元素),引用可能会失效或指向错误的元素。这是因为双端队列在元素插入时可能需要重新分配内存,导致原有引用失效。

涉及状压的BFS

P1379 八数码难题

题目描述

在 3×3 的棋盘上,摆有八个棋子,每个棋子上标有 1 至 8 的某一数字。棋盘中留有一个空格,空格用 0 来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。

输入格式

输入初始状态,一行九个数字,空格用 0 表示。

输出格式

只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数。保证测试数据中无特殊无法到达目标状态数据。

输入输出样例

输入 #1

283104765

输出 #1

4

说明/提示

样例解释

img

图中标有 0 的是空格。绿色格子是空格所在位置,橙色格子是下一步可以移动到空格的位置。如图所示,用四步可以达到目标状态。

并且可以证明,不存在更优的策略。

void solve(){
    string g="123804765";
    string s;cin>>s;

    unordered_map<string,int> mp;
    auto inmap=[&](int x,int y ){
        if(x>=0&&x<3&&y>=0&&y<3)return 1;
        return 0;
    };
    int dx[]={0,0,1,-1};
    int dy[]={1,-1,0,0};

    auto bfs=[&](){
        queue<string> q;
        q.push(s);
        mp[s]=0; // 初始状态步数为0
        while(q.size()){
            string t=q.front();
            q.pop();
            if(t==g){
                return; // 直接返回目标状态的步数
            }

            int loc=t.find('0');
            int u=loc/3,v=loc%3; // 状态压缩之后逆向求压缩之前的坐标...
            rep(i,0,3){
                int x=u+dx[i];
                int y=v+dy[i];
                if(inmap(x,y)){
                    string next = t; // 创建新的字符串进行交换
                    int tem=x*3+y; //求压缩之后的位置...
                    swap(next[loc],next[tem]);
                    if(mp.find(next) == mp.end()){ // 检查是否未访问过, 不用 mp==0 来检测
                        mp[next] = mp[t] + 1;
                        q.push(next);
                    }
                }
            }
        }
        //return -1; // 理论上不会执行到这里,因为题目保证有解
    };

    bfs();
    cout << mp[g] << endl;
}

P2730 [USACO3.2] 魔板 Magic Squares

题目背景

在成功地发明了魔方之后,鲁比克先生发明了它的二维版本,称作魔板。这是一张有 8 个大小相同的格子的魔板:

1234

8765

题目描述

我们知道魔板的每一个方格都有一种颜色。这 8 种颜色用前 8 个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用序列 {1,2,3,4,5,6,7,8} 来表示。这是基本状态。

这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态):

A:交换上下两行;

B:将最右边的一列插入最左边;

C:魔板中央四格作顺时针旋转。

对于每种可能的状态,这三种基本操作都可以使用。

你要编程计算用最少的基本操作完成基本状态到目标状态的转换,输出基本操作序列。

输入格式

只有一行,包括 8 个整数 a1,a2⋯a8(1≤a1,a2⋯a8≤8),用空格分开,不换行,表示目标状态。

输出格式

第一行包括一个整数,表示最短操作序列的长度。

第二行在字典序中最早出现的操作序列,用字符串表示,除最后一行外,每行输出 60 个字符。

输入输出样例

输入 #1

2 6 8 4 5 7 3 1 

输出 #1

7 
BCABCCB

这题主要是输入的处理有点反直觉, 后面debug很久才发现是输入的预处理有问题, BFS 的实现倒是挺简单的, 至少比上面一题简单许多.

void solve(){
    string s,ss;
    string g="12348765";//不是g="12345678";因此s也得变换一步才能对应上
    rep(i,1,4){
        int x;cin>>x;
        s.push_back(x+'0');
    }
    rep(i,1,4){
        int x;cin>>x;
        ss.push_back(x+'0');
    }
    reverse(ss.begin(),ss.end());
    s+=ss;
    
    map<string,string> mp;

    auto bfs=[&](){
        queue<string> q;
        q.push(g);//是从g开始而不是s开始
        mp[g]="";

        while(q.size()){
            auto t=q.front();
            q.pop();
            if(t==s){
                return;
            }
            string next;
            // a
            next=t;
            rep(i,0,3){
                swap(next[i],next[i+4]);
            }
            if(mp.find(next)==mp.end()){
                q.push(next);
                mp[next]=mp[t]+'A';
            }
            //b 
            next=t;
            next[0]=t[3];next[1]=t[0];next[2]=t[1];next[3]=t[2];
            next[4]=t[7];next[5]=t[4];next[6]=t[5];next[7]=t[6];
            if(mp.find(next)==mp.end()){
                q.push(next);
                mp[next]=mp[t]+'B';
            }
            //c
            next=t;
            next[2]=t[1];next[6]=t[2];next[5]=t[6];next[1]=t[5];
            if(mp.find(next)==mp.end()){
                q.push(next);
                mp[next]=mp[t]+'C';
            }
        }
    };
    bfs();

    cout<<mp[s].size()<<endl;
    cout<<mp[s]<<endl;
}

补充: 双向BFS

双向 BFS(双向广度优先搜索)是广度优先搜索(BFS)的一种优化算法,主要用于高效求解起点到终点的最短路径问题。其核心思想是通过 “从起点和终点同时向中间搜索”,减少单向 BFS 中搜索空间爆炸的问题,从而显著提升效率。

一、核心思想

普通 BFS 从起点开始单向逐层扩展,直到找到终点,当搜索空间较大时(如迷宫、社交网络),节点数量会呈指数级增长(例如,每层节点数为前一层的 2 倍,深度为 n 时总节点数为 2ⁿ⁺¹-1)。

双向 BFS 的优化逻辑是:同时从起点和终点出发,两个方向的 BFS 搜索树向中间 “靠近”,当两个方向的搜索在某一节点相遇时,就找到了一条最短路径

此时,两个方向的搜索树深度之和等于单向 BFS 的深度,但总搜索节点数远小于单向(例如,单向深度 n 的节点数为 2ⁿ,双向各深度 n/2 的节点数之和为 2*(2ⁿ/²),远小于 2ⁿ)。

二、算法步骤

双向 BFS 的实现需要维护两个搜索队列(分别对应起点→终点和终点→起点)和两个已访问集合(避免重复搜索),具体步骤如下:

  1. 初始化
    • 定义两个队列 queue1(起点方向)和 queue2(终点方向),分别存入起点 start 和终点 end
    • 定义两个已访问集合 visited1visited2,分别记录 queue1queue2 已搜索的节点(避免重复访问)。
    • 定义两个路径字典 path1path2,分别记录从起点到当前节点、从终点到当前节点的路径(用于最终拼接完整路径)。
  2. 循环搜索
    • 每次选择当前规模较小的队列进行扩展(平衡两个搜索树大小,优化效率)。
    • 从队列中取出一个节点 current,遍历其所有未访问的邻居节点 neighbor
  3. 检查相遇
    • neighbor 已在另一个队列的已访问集合中(例如,neighborvisited2 中),说明两个方向的搜索相遇,此时拼接路径(path1[neighbor] + [neighbor] + reversed(path2[neighbor]))即为最短路径,返回结果。
  4. 扩展节点
    • neighbor 未被当前队列访问过,将其加入队列和已访问集合,并记录路径(path1[neighbor] = path1[current] + [neighbor])。
  5. 终止条件
    • 若找到路径,返回结果;若两个队列均为空(无交集),则说明不存在路径。

三、应用场景

双向 BFS 适用于起点和终点明确、需求解最短路径的场景.

离开中山路 (双向BFS优化)

void solve(){
    int n;cin>>n;
    vector<string> s(n+1);
    rep(i,1,n)cin>>s[i];
    rep(i,1,n)s[i]=' '+s[i];
    int a,b,c,d;cin>>a>>b>>c>>d;

    vector<vector<int>> dist1(n+1,vector<int>(n+1,-1)); // 起点出发的距离
    vector<vector<int>> dist2(n+1,vector<int>(n+1,-1)); // 终点出发的距离

    auto inmap=[&](int x,int y){
        if(x>=1&&x<=n&&y>=1&&y<=n)return 1;
        return 0;
    };
    int dx[]={1,-1,0,0};
    int dy[]={0,0,1,-1};

    auto bidirectional_bfs=[&]()->int{
        if(a==c&&b==d) return 0; // 特判起点和终点相同的情况
        
        queue<pair<int,int>> q1,q2; // 两个方向的队列
        q1.push({a,b}); dist1[a][b]=0; // 起点初始化
        q2.push({c,d}); dist2[c][d]=0; // 终点初始化
        
        while(!q1.empty() || !q2.empty()){
            // 处理起点出发的BFS
            if(!q1.empty()){
                auto t=q1.front(); q1.pop();
                rep(i,0,3){
                    int x=t.first+dx[i];
                    int y=t.second+dy[i];
                    if(inmap(x,y) && s[x][y]!='1'){
                        if(dist1[x][y]==-1){ // 未被起点BFS访问过
                            dist1[x][y]=dist1[t.first][t.second]+1;
                            q1.push({x,y});
                            
                            // 检查是否与终点BFS相遇
                            if(dist2[x][y]!=-1) return dist1[x][y]+dist2[x][y];
                        }
                    }
                }
            }
            
            // 处理终点出发的BFS
            if(!q2.empty()){
                auto t=q2.front(); q2.pop();
                rep(i,0,3){
                    int x=t.first+dx[i];
                    int y=t.second+dy[i];
                    if(inmap(x,y) && s[x][y]!='1'){
                        if(dist2[x][y]==-1){ // 未被终点BFS访问过
                            dist2[x][y]=dist2[t.first][t.second]+1;
                            q2.push({x,y});
                            
                            // 检查是否与起点BFS相遇
                            if(dist1[x][y]!=-1) return dist1[x][y]+dist2[x][y];
                        }
                    }
                }
            }
        }
        return -1; // 无法到达
    };
    
    cout<<bidirectional_bfs()<<endl;
}

八数码难题 (双向BFS优化)

void solve(){
    string start; cin >> start;
    string target = "123804765";

    if(start == target) {
        cout << 0 << endl;
        return;
    }

    unordered_map<string, int> dist1, dist2; // 分别记录两个方向的距离
    queue<string> q1, q2; // 两个方向的队列

    auto inmap = [&](int x, int y) {
        return x >= 0 && x < 3 && y >= 0 && y < 3;
    };

    int dx[] = {0, 0, 1, -1};
    int dy[] = {1, -1, 0, 0};

    auto bidirectional_bfs = [&]() {
        q1.push(start); dist1[start] = 0;
        q2.push(target); dist2[target] = 0;

        while(!q1.empty() || !q2.empty()) {
            // 扩展正向BFS
            if(!q1.empty()) {
                string current = q1.front(); q1.pop();
                int loc = current.find('0');
                int u = loc / 3, v = loc % 3;

                for(int i = 0; i < 4; i++) {
                    int x = u + dx[i];
                    int y = v + dy[i];
                    if(inmap(x, y)) {
                        string next = current;
                        int new_loc = x * 3 + y;
                        swap(next[loc], next[new_loc]);

                        if(dist1.find(next) == dist1.end()) {
                            dist1[next] = dist1[current] + 1;
                            q1.push(next);

                            // 检查是否相遇
                            if(dist2.find(next) != dist2.end()) {
                                return dist1[next] + dist2[next];
                            }
                        }
                    }
                }
            }

            // 扩展反向BFS
            if(!q2.empty()) {
                string current = q2.front(); q2.pop();
                int loc = current.find('0');
                int u = loc / 3, v = loc % 3;

                for(int i = 0; i < 4; i++) {
                    int x = u + dx[i];
                    int y = v + dy[i];
                    if(inmap(x, y)) {
                        string next = current;
                        int new_loc = x * 3 + y;
                        swap(next[loc], next[new_loc]);

                        if(dist2.find(next) == dist2.end()) {
                            dist2[next] = dist2[current] + 1;
                            q2.push(next);

                            // 检查是否相遇
                            if(dist1.find(next) != dist1.end()) {
                                return dist1[next] + dist2[next];
                            }
                        }
                    }
                }
            }
        }
        // return -1; // 题目保证有解,不会执行到这里
    };

    cout << bidirectional_bfs() << endl;
}
posted @ 2025-07-15 19:08  byxxx  阅读(9)  评论(0)    收藏  举报