BFS思路学习
最近学习了一篇BFS的文章,帮助很大,如下:
版权声明:本文为CSDN博主「sooner高」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/g11d111/article/details/76169861
我把其中我认为重要的点罗列出来,以供之后参考学习:
BFS简介
广度优先搜索(也称宽度优先搜索,缩写BFS,以下采用广度来描述)是连通图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,因此得名。一般可以用它做什么呢?一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。
BFS的基本思路
常常我们有这样一个问题:从一个起点开始要到一个终点,我们要找寻一条最短的路径。以下图为例,如果我们要求V0到V6的一条最短路(假设走一个节点按一步来算),我们明显看出这条路径就是V0−>V2−>V6,而不是V0−>V3−>V5−>V6。先想想你自己刚刚是怎么找到这条路径的:首先看跟V0直接连接的节点V1、V2、V3,发现没有V6,进而再看刚刚V1、V2、V3的直接连接节点分别是:{V0、V1 、V2 、V 3、V4}、{V0 、V1 、V6}、{V0 、V1 、V5}(这里画删除线的意思是那些顶点在我们刚刚的搜索过程中已经找过了,我们不需要重新回头再看他们了)。这时候我们从V2V2的连通节点集中找到了V6V6,那说明我们找到了这条V0到V6的最短路径:V0−>V2−>V6V0−>V2−>V6,虽然你再进一步搜索V5的连接节点集合后会找到另一条路径V0−>V3−>V5−>V6,但显然他不是最短路径,我们将其扔掉。
可以采用示例图来说明这个过程:
在搜索的过程中,初始所有节点是白色(代表了所有点都还没开始搜索),把起点V0标志成灰色(表示即将辐射V0),下一步搜索的时候,我们把所有的灰色节点访问一次,然后将其变成黑色(表示已经被辐射过了),进而再将他们所能到达的节点标志成灰色(因为那些节点是下一步搜索的目标点了),但是这里有个判断,就像刚刚的例子,当访问到V1V1节点的时候,它的下一个节点应该是V0V0和V4V4,但是V0V0已经在前面被染成黑色了,所以不会将它染灰色。这样持续下去,直到目标节点V6V6被染灰色,说明了下一步就到终点了,没必要再搜索(染色)其他节点了,此时可以结束搜索了,整个搜索就结束了。然后根据搜索过程,反过来把最短路径找出来,下图中把最终路径上的节点标志成绿色。
广度优先搜索流程图
BFS核心算法
bool BFS(Node& Vs, Node& Vd){
queue<Node> Q;
Node Vn, Vw;
int i;
//初始状态将起点放进队列Q
Q.push(Vs);
hash(Vw) = true;//设置节点已经访问过了!
while (!Q.empty()){//队列不为空,继续搜索!
//取出队列的头Vn
Vn = Q.front();
//从队列中移除
Q.pop();
while(Vw = Vn通过某规则能够到达的节点){
if (Vw == Vd){//找到终点了!
//把路径记录,这里没给出解法
return true;//返回
}
if (isValid(Vw) && !visit[Vw]){
//Vw是一个合法的节点并且为白色节点
Q.push(Vw);//加入队列Q
hash(Vw) = true;//设置节点颜色
}
}
}
return false;//无解
}
对于一个题目来说,要标志节点是否访问过,用数组是一种很快速的方法,但有时数据量太大,很难用一个大数组来记录时,采用hash set是最好的做法。实际上visit数组在这里也是充当hash set的作用。
上面是CSDN博主「sooner高」的原创文章,下面写一些我自己的理解,
bool BFS(Node& Vs, Node& Vd){
queue<Node> Q;
Node Vn, Vw;
int i;
//初始状态将起点放进队列Q
Q.push(Vs);
hash(Vw) = true;//设置节点已经访问过了!
/*这里的hash()不是一个具体的函数,而是说这里利用hash思想,具体实现
时可以利用数组或者set(集合)*/
while (!Q.empty()){//队列不为空,继续搜索!
//取出队列的头Vn
Vn = Q.front();
//从队列中移除
Q.pop();
while(Vw = Vn通过某规则能够到达的节点){/*实际写代码(如迷宫问题),
一般是用for()循环来遍历当前点能够到达的节点*/
if (Vw == Vd){//找到终点了!
//把路径记录,这里没给出解法
return true;//返回
}
if (isValid(Vw) && !visit[Vw]){
//Vw是一个合法的节点并且为白色节点
Q.push(Vw);//加入队列Q
hash(Vw) = true;//设置节点颜色
}
}
}
return false;//无解
}
BFS具体实现及应用学习
上面的文章主要是提供了BFS的思路,当我真正想要实现时发现我不会写,于是我又学习了一篇BFS具体实现及应用的文章,如下:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sigd/article/details/123890402
我把其中重要的点罗列出来,以供之后参考:
BFS算法虽然出自图结构,但其常用的领域却不是解决图论相关问题。一些常见的问题形式如(1)走迷宫最短路径(2)数字按规则转换的最少次数(3)棋盘上某个棋子N步后能到达的位置总数(4)病毒扩散计算(5)图像中连通块的计算。小结:BFS算法常用于求最短的步数或者求扩散性质的区域问题。
BFS实现模板
在算法处理过程中,新生成的结点(状态)要与前面所有已经产生结点比较,以免出现重复结点,陷入死循环。一般用以一个标志数组来标记某个结点是否已经处理过,而这个数组同样可以用于记录步数。具体可参见下面例题,一个标准的迷宫最短路径问题。
#include <iostream>
using namespace std;
char a[100][100];
int n,m,v[100][100]={0};/**< v标志数组,同时也用于记录步数 */
int dx[4]={0,0,1,-1}; int dy[4]={1,-1,0,0};
void bfs(int x,int y)
{
int i,j;/**< 两个队列qx,qy分别存储横纵坐标 */
int qx[100005],qy[100005],fx=0,rx=0,fy=0,ry=0;
qx[rx++]=1,qy[ry++]=1;/**< */
v[1][1]=1;/**< 迷宫起点步数记为1 */
while(fx!=rx)
{
x=qx[fx++],y=qy[fy++];
for(i=0;i<4;i++)
{
int newx=x+dx[i],newy=y+dy[i];/**< bfs三要素,合法性+可访问+未标记 */
if(newx>=1&&newx<=n&&newy>=1&&newy<=m&&a[newx][newy]=='0'&&v[newx][newy]==0)
{
v[newx][newy]= v[x][y]+1;/**< 因为(newx,newy)是从(x,y)走一步到达,因为步数+1 */
qx[rx++]=newx,qy[ry++]=newy;
}
}
}
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0);
int i,j;
cin>>n>>m;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
cin>>a[i][j];
bfs(1,1);
cout<<v[n][m]-1;
return 0;
}
路径处理
当需要记录路径时,**每生成一个子结点,就要存储指向它们父亲结点信息,此时使用的结构本质上是一个反向链式结构。**以上述迷宫问题为例,定义一个数组存储(newx,newy)的父节点(x,y),反向遍历即可得到路径,反向遍历用递推实现最为容易。
新增一个函数输出路径:
void printPath(int x,int y)
{
if(x==0&&y==0)
return;
printPath(fa[x][y][0],fa[x][y][1]);/**< 先递推父节点,再输出自己 */
cout<<x<<' '<<y<<endl;
}
上面是CSDN博主「sooner高」的原创文章,下面写一些我自己的理解,
首先是迷宫问题(最短路径)的具体实现:
参考文章中使用fx和rx,fx代表当前访问的结点,rx是队列最后一个结点,用fx != rx来不断进行循环,我认为采用队列更加简单,下面是我的实现:
BFS实现
#include <iostream>
#include <queue>
using namespace std;
char a[100][100];//用a数组来存储迷宫
int vis[100][100];//注意这里vis数组不仅起标记数组的作用,而且还用于记录步数。
int dx[4] = {0, 0, -1, 1};//注意这里不是乱写的,而是dx[0]对应dy[0],表示向上移动一格,dx[1]对应dy[1],表示向下移动一格
int dy[4] = {1, -1, 0, 0};
queue<int> qx;//队列qx用于存储横坐标
queue<int> qy;//队列qy用于存储纵坐标
int n, m;
void bfs(int x, int y);
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> a[i][j];
}
}
bfs(1, 1);
cout << vis[n][m] - 1;//这里-1是因为我们在bfs中把迷宫起点步数记为1,本来应该是0,所以这里-1,而这里-1的好处是如果vis[n][m]为0(没法到达终点),则按要求输出-1
return 0;
}
void bfs(int x, int y){
//初始状态将起点放进队列
qx.push(x);
qy.push(y);
vis[x][y] = 1;//标记已经走过[1][1],并且把迷宫起点步数记为1
while(!qx.empty()){//队列不为空,继续搜索,队列为空代表所有能遍历的点都遍历完了
x = qx.front();
qx.pop();
y = qy.front();//这一步相当于上面示例图中把[x][y]变为黑色
qy.pop();
for(int i = 0; i < 4; i++){//遍历周围的点,判断是否可以辐射
int newx = x + dx[i];
int newy = y + dy[i];
if(newx >= 1 && newx <= n && newy >= 1 && newy <= m && a[newx][newy] == '0' && vis[newx][newy] == 0){//点可被辐射三要素:合法性+可访问+未标记
vis[newx][newy] = vis[x][y] + 1;//[newx][newy]是由[x][y]走一步到达,所以步数+1
qx.push(newx);//由于[newx][newy]可被辐射,所以把[newx][newy]加入队列
qy.push(newy);
}
}
}
}
路径处理:
在原来的代码上增加路径记录数组(记录上一结点)和路径输出数组(反向遍历得到从起点到终点的过程)
#include <iostream>
#include <queue>
using namespace std;
char a[100][100];//用a数组来存储迷宫
int vis[100][100];//注意这里vis数组不仅起标记数组的作用,而且还用于记录步数。
int dx[4] = {0, 0, -1, 1};//注意这里不是乱写的,而是dx[0]对应dy[0],表示向上移动一格,dx[1]对应dy[1],表示向下移动一格
int dy[4] = {1, -1, 0, 0};
queue<int> qx;//队列qx用于存储横坐标
queue<int> qy;//队列qy用于存储纵坐标
int n, m;
int fa[100][100][2];//定义一个数组fa用来存储上一步的结点
void bfs(int x, int y);
void printPath(int x, int y);
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> a[i][j];
}
}
bfs(1, 1);
cout << vis[n][m] - 1 << endl;//这里-1是因为我们在bfs中把迷宫起点步数记为1,本来应该是0,所以这里-1,而这里-1的好处是如果vis[n][m]为0(没法到达终点),则按要求输出-1
printPath(n, m);
return 0;
}
void bfs(int x, int y){
//初始状态将起点放进队列
qx.push(x);
qy.push(y);
vis[x][y] = 1;//标记已经走过[1][1],并且把迷宫起点步数记为1
while(!qx.empty()){//队列不为空,继续搜索,队列为空代表所有能遍历的点都遍历完了
x = qx.front();
qx.pop();
y = qy.front();//这一步相当于上面示例图中把[x][y]变为黑色
qy.pop();
for(int i = 0; i < 4; i++){//遍历周围的点,判断是否可以辐射
int newx = x + dx[i];
int newy = y + dy[i];
if(newx >= 1 && newx <= n && newy >= 1 && newy <= m && a[newx][newy] == '0' && vis[newx][newy] == 0){//点可被辐射三要素:合法性+可访问+未标记
vis[newx][newy] = vis[x][y] + 1;//[newx][newy]是由[x][y]走一步到达,所以步数+1
qx.push(newx);//由于[newx][newy]可被辐射,所以把[newx][newy]加入队列
qy.push(newy);
fa[newx][newy][0] = x;
fa[newx][newy][1] = y;//记录上一步结点
}
}
}
}
void printPath(int x, int y){
if(x == 1 && y == 1){
cout << x << " " << y << endl;
return;
}
else{
printPath(fa[x][y][0], fa[x][y][1]);//先递归上一结点,再输出自己
cout << x << " " << y << endl;
}
}
运行结果: