BFS最短路径的两种打印方法

预告:我用两年写的新书《算法竞赛》,已于2022年2月交给清华大学出版社,预计于2022年7月出版。
《算法竞赛》是一本“大全”,内容覆盖“基础-中级-高级”,篇幅700页左右。部分知识点的草稿已经在本博客发表。
本篇博客节选自新书《算法竞赛》的“3.4 BFS与最短路”。

1、BFS求最短路

   最短路问题是最有名的图论问题,有很多不同的场景和算法。
   在一种特殊的场景中,BFS也是极为优秀的最短路算法,这种场景就是:所有的相邻两个点的距离相等,一般把这个距离看成1。此时BFS是最优的最短路径算法,查找一次从起点到终点的最短距离的计算复杂度是O(m) ,m是图上边的数量,因为需要对每条边做一次检查。
   如果两点之间距离不相等,就不能用BFS了,需要用Dijkstra等通用算法。
   BFS的特点是逐层扩散,也就是按最短路扩散出去。往BFS的队列中加入邻居结点时,是按距离起点远近的顺序加入的:先加入距离起点为1的邻居结点,加完之后,再加入距离为2的邻居结点,等等。搜完一层,才会继续搜下一层。一条路径是从起点开始,沿着每一层逐步往外走,每多一层,路径长度就增加1。那么,所有长度相同的最短路径都是从相同的层次扩散出去的。
   求最短路径时,常见的问题有两个:
   (1)最短路径有多长?答案显然是唯一的;
   (2)最短路径经过了哪些点?由于最短路径可能不只一条,所以题目往往不要求输出,如果要求输出,一般是要求输出字典序最小的那条路径。
   下面用一道例题介绍最短路径的计算和最短路径的打印。


2019年省赛真题 迷宫 https://www.lanqiao.cn/problems/602/learning/
题目描述:下图给出了一个迷宫的平面图,其中标记为1 的为障碍,标记为0 的为可以通行的地方。
010000
000100
001001
110000
迷宫的入口为左上角,出口为右下角,在迷宫中,只能从一个位置走到这个它的上、下、左、右四个方向之一。对于上面的迷宫,从入口开始,可以按DRRURRDDDR 的顺序通过迷宫,一共10 步。其中D、U、L、R 分别表示向下、向上、向左、向右走。对于下面这个更复杂的迷宫(30 行50 列),请找出一种通过迷宫的方式,其使用的步数最少,在步数最少的前提下,请找出字典序最小的一个作为答案。
请注意在字典序中D<L<R<U。


  本题是基本的BFS搜索最短路。BFS是最优的算法,每个点只需搜一次,即进队列和出队列各一次。
  题目要求返回字典序最小的最短路径,那么只要在每次扩散下一层(往BFS的队列中加入下一层的结点)时,都按字典序“D<L<R<U”的顺序来加下一层的结点,那么第一个搜到的最短路径就是字典序最小的。
  本题的关键是路径打印,下面给出两种打印方法。

2、路径打印的简单方法

  简单方法,适合小图。
  每扩展到一个点v,都在v上存储从起点s到v的完整路径path。到达终点t时,就得到了从起点s到t的完整路径。
  在下面的代码中,在每个结点上记录从起点到这个点的路径。到达终点后,用cout<<now.path<<endl;就打印出了完整路径。
  这样做的缺点是会占用大量空间,因为每个点上都存储了完整的路径。

#include<bits/stdc++.h>
using namespace std;
struct node{
    int x;
    int y;
//(1)简单方法:
    string path;    //path,记录从起点(0,0)到这个点(x,y)的完整路径
};
char mp[31][51];     //用矩阵存地图
char k[4]={'D','L','R','U'};  //字典序
int dir[4][2]={{1,0},{0,-1},{0,1},{-1,0}};   //4个方向
int vis[30][50];        //标记。vis=1: 已经搜过,不用再搜

void bfs(){
    node start; 
    start.x=0;  
    start.y=0;
//(1)简单方法:
    start.path="";
    vis[0][0]=1;               //标记起点被搜过
    queue<node>q;
    q.push(start);             //把第一个点放进队列,开始BFS
    while(!q.empty()){
        node now = q.front();  //取出队首
        q.pop();
        if(now.x==29 && now.y==49){ //第一次达到终点,这就是字典序最小的最短路径
//(1)简单方法:打印完整路径
            cout << now.path << endl;
            return;
        }
        for(int i=0;i<4;i++){  //BFS:扩散邻居结点
            node next;
            next.x = now.x + dir[i][0];  next.y = now.y + dir[i][1];
            if(next.x<0||next.x>=30||next.y<0||next.y>=50)  //越界了
                continue;
            if(vis[next.x][next.y]==1 || mp[next.x][next.y]=='1')
                continue;           //vis=1:已经搜过;  mp=1:是障碍
            vis[next.x][next.y]=1;  //标记被搜过
//(1)简单方法:记录完整路径:复制上一个点的路径,加上这一步
            next.path = now.path + k[i];
            q.push(next);
        }
    }
}
int main(){
    for(int i=0;i<30;i++)  
    	cin >> mp[i];  //读题目给的地图数据
    bfs();
}

3、路径打印的标准方法

  标准方法,适合大图。
  其实不用在每个结点上存储完整路径,而是在每个点上记录它的前驱结点就够了,这样从终点能一步步回溯到起点,得到一条完整路径。称这种路径记录方法为“标准方法”。
  注意看代码中的print_path(),它是递归函数,先递归再打印。从终点开始,回溯到起点后,再按从起点到终点的顺序,正序打印出完整路径。

#include<bits/stdc++.h>
using namespace std;
struct node{
    int x;
    int y;
};
char mp[31][51];  //存地图
char k[4]={'D','L','R','U'};  //字典序
int dir[4][2]={{1,0},{0,-1},{0,1},{-1,0}};
int vis[30][50];  //标记。vis=1: 已经搜过,不用再搜

//(2)标准方法:
char pre[31][51];        //   用于查找前驱点。例如pre[x][y] = ‘D’,表示上一个点
                         //往下走一步到了(x,y),那么上一个点是(x-1,y)
void print_path(int x,int y){      //打印路径:从(0,0)到(29,49)
    if(x==0 && y==0)    return;      //回溯到了起点,递归结束,返回
    if(pre[x][y]=='D')  print_path(x-1,y);   //回溯,往上 U
    if(pre[x][y]=='L')  print_path(x,  y+1); //回溯,往右 R
    if(pre[x][y]=='R')  print_path(x,  y-1);
    if(pre[x][y]=='U')  print_path(x+1,y);
    printf("%c",pre[x][y]);                  //最后打印的是终点
}
void bfs(){
    node start; 
    start.x=0;  
    start.y=0;
    vis[0][0]=1;               //标记起点被搜过
    queue<node>q;
    q.push(start);             //把第一个点放进队列,开始BFS
    while(!q.empty()){
        node now = q.front();  //取出队首
        q.pop();
        if(now.x==29 && now.y==49){ //第一次达到终点,这就是字典序最小的最短路径
//(2)标准方法:打印完整路径,从终点回溯到起点,打印出来是从起点到终点的正序
            print_path(29,49);
            return;
        }
        for(int i=0;i<4;i++){  //扩散邻居结点
            node next;
            next.x = now.x + dir[i][0];  next.y = now.y + dir[i][1];
            if(next.x<0||next.x>=30||next.y<0||next.y>=50)  //越界了
                continue;
            if(vis[next.x][next.y]==1 || mp[next.x][next.y]=='1')
                continue;           //vis=1:已经搜过;  mp=1:是障碍
            vis[next.x][next.y]=1;  //标记被搜过
//(2)标准方法:记录点(x,y)的前驱
            pre[next.x][next.y] = k[i];
            q.push(next);
        }
    }
}
int main(){
    for(int i=0;i<30;i++)  
    	cin >> mp[i];  //读题目给的地图数据
    bfs();
}
  • 16
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
### 回答1: 以下是 Python 代码实现 BFS 计算最短路径: ```python from collections import deque def bfs(graph, start, end): # 使用队列来进行 BFS queue = deque() queue.append(start) # 用于记录已经访问过的节点 visited = set() visited.add(start) while queue: # 取出队列中的第一个节点 node = queue.popleft() # 如果该节点是目标节点,结束搜索 if node == end: return True # 遍历相邻节点,并将其加入队列中 for neighbor in graph[node]: if neighbor not in visited: queue.append(neighbor) # 标记已经访问过的节点 visited.add(neighbor) # 没有找到目标节点,返回 False return False # 构建图 graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } # 测试 print(bfs(graph, 'A', 'F')) # 输出 True print(bfs(graph, 'D', 'C')) # 输出 False ``` 注意,在上述代码中,我们只是判断了起点能否到达终点,如果需要计算最短路径,需要将 BFS 程序进行适当的修改。比如,我们可以使用一个字典来记录每个节点的前一个节点,然后从终点往回遍历这个字典,就可以得到最短路径了。 ### 回答2: 广度优先搜索(BFS)是一种图遍历算法,用于在无权图中计算最短路径。以下是一个使用Python实现BFS算法来计算最短路径的简单示例: ```python from collections import deque def bfs(graph, start, end): # 创建一个队列,用于存储待访问的节点 queue = deque() # 创建一个集合,用于记录已访问的节点 visited = set() # 存储每个节点的父节点,用于最后回溯路径 parent = {} # 将起始节点加入队列和已访问集合 queue.append(start) visited.add(start) while queue: node = queue.popleft() # 判断是否找到目标节点 if node == end: break # 遍历当前节点的邻居节点 for neighbor in graph[node]: if neighbor not in visited: queue.append(neighbor) visited.add(neighbor) # 记录邻居节点的父节点 parent[neighbor] = node # 回溯最短路径 shortest_path = [] current_node = end while current_node != start: shortest_path.append(current_node) current_node = parent[current_node] shortest_path.append(start) # 反转路径列表 shortest_path = shortest_path[::-1] return shortest_path # 定义一个无向图,使用字典表示邻接表 graph = { 'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A', 'E'], 'D': ['B', 'E', 'F'], 'E': ['C', 'D', 'F'], 'F': ['D', 'E'] } start_node = 'A' end_node = 'F' shortest_path = bfs(graph, start_node, end_node) print("最短路径:", shortest_path) ``` 上述代码通过字典表示的邻接表来表示一个无向图。bfs函数接受三个参数:图graph、起始节点start和目标节点end。函数使用队列来实现BFS,首先将起始节点加入队列和已访问集合,然后对队列中的节点进行遍历,依次访问其邻居节点。当找到目标节点时,结束遍历。然后通过回溯父节点的方式,找到最短路径。最后将路径列表进行反转,即可得到从起始节点到目标节点的最短路径。最后将最短路径打印出来。 运行上述代码,将输出以下结果: 最短路径: ['A', 'C', 'E', 'F'] ### 回答3: BFS(Breadth First Search)即广度优先搜索算法,在计算最短路径中应用较广。下面将使用Python编程语言来说明BFS计算最短路径的过程。 首先,我们需要创建一个图来表示路径的关系。可以用字典来表示,字典的键表示节点的名字,值表示与该节点相连的其他节点的列表。例如,如果有两个节点A和B相连,我们可以使用字典`graph = {'A': ['B'], 'B': ['A']}`来表示。 接下来,我们需要定义一个函数来计算最短路径。函数使用一个队列来存储待处理的节点。开始时,我们将起始节点放入队列中,并创建一个集合来存储已经访问过的节点。然后,我们开始一个循环,直到队列为空。在每次循环中,我们从队列中取出一个节点,并检查该节点是否为目标节点。如果是,我们找到了最短路径,直接返回。否则,我们将该节点标记为已访问,并将与之相连的尚未访问过的节点加入队列。 最后,如果循环结束时仍然没有找到目标节点,则表示不存在从起始节点到目标节点的路径。 下面是一个使用BFS算法计算最短路径的示例代码: ``` def bfs(graph, start, end): queue = [[start]] visited = set() while queue: path = queue.pop(0) node = path[-1] if node == end: return path if node not in visited: neighbors = graph[node] for neighbor in neighbors: new_path = list(path) new_path.append(neighbor) queue.append(new_path) visited.add(node) return None graph = {'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E']} start_node = 'A' end_node = 'F' shortest_path = bfs(graph, start_node, end_node) if shortest_path: print('最短路径为:', shortest_path) else: print('找不到最短路径') ``` 在上述示例中,我们定义了一个名为`bfs`的函数,它使用了一个`graph`字典来表示图的结构。函数将起始节点和目标节点作为参数传入,并返回最短路径。 希望这段代码能够帮助你理解如何使用BFS算法来计算最短路径

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗勇军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值