BFS 广度优先搜索 ( Breadth-First-Search )
广度优先搜索算法(Breadth First Search),又称为"宽度优先搜索", BFS是用于图的查找算法(要求能用图表示出问题的关联性)。
BFS可用于解决2类问题:
1.从A出发是否存在到达B的路径;DFS也可求
2.从A出发到达B的最短路径;数据小20以内的话, DFS也不是不可以 题眼
整体思路
其思路为从图上一个节点出发,访问先访问其直接相连的子节点,若子节点不符合,再问其子节点的子节点,按级别顺序(一层一层)依次访问,直到访问到目标节点。
步骤
- 起始:将起点(源点,树的根节点)放入队列中
- 扩散:从队列中取出队头的结点,将它的相邻结点放入队列,不断重复这一步
- 终止:当队列为空时,说明我们遍历了所有的能到的结点,整个图能到的点都被搜索了一遍
时空复杂度
时间复杂度 : 最差情形下,BFS必须寻找所有到可能节点的所有路径,因此其时间复杂度为O(|V| + |E|),其中|V|是节点的数目,而|E|是图中边的数目。
空间复杂度 : BFS的空间复杂度为 O(B^h),其中B是最大分支系数,而h是树的最长路径长度(树的高度) 。由于对空间的大量需求,因此BFS并不适合解非常大的问题。
对于所有边长度相同的情况,比如地图的模型,bfs第一次遇到目标点,此时就一定是从根节点到目标节点最短的路径(因为每一次所有点都是向外扩张一步,你先遇到,那你就一定最短)。bfs先找到的一定是最短的。但是如果是加权边的话这样就会出问题了,bfs传回的是经过边数最少的解,但是因为加权了,这个解到根节点的距离不一定最短。比如1000+1000是只有两段,1+1+1+1有4段,由于bfs返回的经过边数最少的解,这里会返回总长度2000的那个解,显然不是距离最短的路径。此时我们就应该采用Dijkstra最短路算法解决加权路径的最短路了。
SPFA
例题
走迷宫
acw844. 走迷宫 , 若打不开这个题就看这个 P1746 离开中山路
//比赛中可以直接用万能头文件
#include<iostream>
#include<string>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
#include<cstring>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=110,M=1e3+10;
//常用的数组
int a[N],pre[N];
int n,m;
int g[N][N];//存地图
int dist[N][N];//存每个点到起点的距离
queue<PII>q;//队列
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int dfs(int x1,int y1)
{
memset(dist,-1,sizeof dist);//距离都初始化为-1
q.push({x1,y1});
dist[x1][y1]=0;
while (q.size())//队列不空,继续循环
{
auto t=q.front();//取出队头
q.pop();//弹出
for(int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<1||a>n||b<1||b>m)continue;
if(g[a][b]!=0)continue;
if(dist[a][b]>0)continue;
q.push({a,b});
dist[a][b]=dist[t.x][t.y]+1;
}
}
return dist[n][m];
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>g[i][j];
int res=dfs(1,1);
cout<<res<<endl;
return 0;
}
习题 - 简单迷宫问题
离开中山路
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//'\n'输出更快一些,而且是分别显示每一行,建议scanf与printf配套
#define endl '\n'
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=1010,M=1e3+10;
int n;
int dist[N][N];
queue<PII>q;
char g[N][N];
int x1,y1,x2,y2;
//常用的数组
int a[N],pre[N];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int bfs(int x1,int y1)
{
memset(dist,-1, sizeof dist);
q.push({x1,y1});
dist[x1][y1]=0;
while (q.size())
{
auto t=q.front();
q.pop();
for(int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<1||a>n||b<1||b>n)continue;
if(g[a][b]!='0')continue;
if(dist[a][b]>=0)continue;//表示走过了,不能重复走
q.push({a,b});
dist[a][b]=dist[t.x][t.y]+1;
if(dist[x2][y2]>0)return dist[x2][y2];
}
}
return -1;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>g[i][j];
cin>>x1>>y1>>x2>>y2;
int res=bfs(x1,y1);
cout<<res<<endl;
return 0;
}
马的遍历
P1443 马的遍历 1
用数组模拟队列
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=410,M=1e3+10;
//常用的数组
int a[N],pre[N];
int n,m;
int dist[N][N];
PII q[N*N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
void bfs(int x1,int y1)
{
memset(dist,-1, sizeof(dist));
q[0]={x1,y1};
dist[x1][y1]=0;
int hh=0,tt=0;//tt本来应该等于-1,因为上面已经加入了一个元素,所以tt=0
while (hh<=tt)
{
//auto t=q[hh]; 取出队头 t=q.front();
//hh++; q.pop();
auto t=q[hh++];
for(int i=0;i<8;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<1||a>n||b<1||b>m)continue;
if(dist[a][b]>=0)continue;//表示已经走过了,不能重复走
dist[a][b]=dist[t.x][t.y]+1;
q[++tt]={a,b};
}
}
}
signed main()
{
int x1,y1;
cin>>n>>m>>x1>>y1;
bfs(x1,y1);
for(int i=1;i<=n;i++) {
for (int j = 1; j <= m; j++)
printf("%-5d",dist[i][j]);
cout << endl;
}
return 0;
}
好奇怪的游戏
--------------P1747 好奇怪的游戏 (跑2次) 0
Bronze Lilypad Pond B
--------------P2385 Bronze Lilypad Pond B 可控马步
多源BFS
从多个起点同时开始
血色先锋队
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=510,M=1e3+10;
//常用的数组
int n,m,a,b;
int dist[N][N];
PII q[N*N];
int hh=0,tt=-1;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void bfs()
{
while (hh<=tt)
{
auto t=q[hh++];
for(int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<1||a>n||b<1||b>m)continue;
if(dist[a][b]>=0)continue;
dist[a][b]=dist[t.x][t.y]+1;
q[++tt]={a,b};
}
}
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>a>>b;
memset(dist,-1,sizeof dist);
while (a--)
{
int x1,y1;
cin>>x1>>y1;
q[++tt]={x1,y1};
dist[x1][y1]=0;
}
bfs();
//bfs扫完直接输入领主的位置,直接输出他在第几轮被感染
while (b--)
{
int x1,y1;
cin>>x1>>y1;
cout<<dist[x1][y1]<<endl;
}
return 0;
}
或者用这种方式存领主
矩阵距离
--------------------acw173.矩阵距离 0
染色问题
填涂颜色
P1162 填涂颜色9 1
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=35,M=1e3+10;
int n;
int g[N][N];
bool st[N][N];
PII q[N*N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void bfs(int x1,int y1)
{
q[0]={x1,y1};
st[x1][y1]=true;
int hh=0,tt=0;
while (hh<=tt)
{
auto t=q[hh++];//取出队头
for(int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<0||a>n+1||b<0||b>n+1)continue;
if(g[a][b]==1)continue;
if(st[a][b])continue;
st[a][b]= true;
q[++tt]={a,b};
}
}
return;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>g[i][j];
bfs(0,0);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(g[i][j]==0&&!st[i][j])
g[i][j]=2;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) {
cout << g[i][j] << " ";
}
cout<<endl;
}
return 0;
}
//bfs后重新遍历一遍,如果这个点等于0,并且没有被标记过,就把这个点变成2,
//因为不能走1,所以没被标记的,都是需要染色的
//在圈外加一圈0,避免(1,1)是在1上,周围都是1,无法移动,从(0,0)开始,到(n+1,n+1)
拯救oibh总部
-----------------------P1506 拯救oibh总部
Forgery
-----------------------CF1059B Forgery
有外界干扰的迷宫问题
Meteor Shower S
P2895 Meteor Shower S 天降陨石 1
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=310,M=1e3+10;
int m;
int dist[N][N];
int fire[N][N];//记录流星砸到(i,j)的时间
PII q[N*N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs()
{
q[0]={0,0};
dist[0][0]=0;
int hh=0,tt=0;
while(hh<=tt)
{
auto t=q[hh++];
for(int i=0;i<4;i++)
{
int a=t.x+dx[i],b=t.y+dy[i];
if(a<0||b<0)continue;
if(dist[a][b])continue;
if(dist[t.x][t.y]+1>=fire[a][b])continue;
dist[a][b]=dist[t.x][t.y]+1;
q[++tt]={a,b};
if(fire[a][b]>1e9)return dist[a][b];
}
}
return -1;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>m;
memset(fire,0x3f,sizeof fire);
while (m--)
{
int x1,y1,t;
cin>>x1>>y1>>t;
fire[x1][y1]=min(t,fire[x1][y1]);
for(int i=0;i<4;i++)
{
int a=x1+dx[i],b=y1+dy[i];
if(a<0||a>301||b<0||b>301)continue;//因为流星在300,他的焦土在301
fire[a][b]=min(t,fire[a][b]);
}
}
int res=bfs();
cout<<res<<endl;
return 0;
}
//先对流星砸过的土地预处理,把流星砸过来的时间标记在地图上
路障
--------------------P3395 路障 天降路障 0.5
看起来比较困难的BFS
汽车拉力比赛
机器人搬重物
--------------P1126 机器人搬重物
魔板 Magic Squares
双端队列广搜
小明的游戏
棋盘
--------------------------P3596 棋盘
Switch the Lamp On
--------------------------P4667 Switch the Lamp On
双向广搜
离开中山路
八数码难题
献给阿尔吉侬的花束
------------acw1101.献给阿尔吉侬的花束
骑士的移动
------------UVA439 骑士的移动
奇怪的电梯
------------P1135 奇怪的电梯
字串变换
------------P1032 字串变换
Corn Maze S
------------P1825 Corn Maze S
------------more and more…知道起点和终点状态的都可以用双向BFS来优化时间
作业 (上节课DFS过不去, 这节课可以用BFS试一试~)
奇怪的电梯
P1135 奇怪的电梯 dfs暴力过不去
Flood fill — 找连通块
Lake Counting S
求细胞数量
海战
家族
UCV2013H - Slick
迷宫问题
入门
迷宫
马的遍历
好奇怪的游戏
Mzc和男家丁的游戏
关于memset和0x3f
int a[100];
memset(a,0x3f,sizeof(a) );
0x3f=0011 1111=63
C++中int型变量所占的位数为4个字节,即32位
0x3f显然不是int型变量中单个字节的最大值,应该是0x7f=0111 1111 B
那为什么要赋值0x3f ??
int
1.作为无穷大使用
因为4个字节均为0x3f时,0x3f3f3f3f的十进制是1061109567,也就是10^ 9级别的(和0x7fffffff一个数量级),而一般场合下的数据都是小于10^9的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。
2.可以保证无穷大加无穷大仍然不会超限。
另一方面,由于一般的数据都不会大于10^9,所以当我们把无穷大加上一个数据时,它并不会溢出(这就满足了**“无穷大加一个有穷的数依然是无穷大”),事实上0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit int_MAX的表示范围,所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求**。
首先要知道memset函数是以字节为单位进行赋值的;
void memset(void *s, int ch, size_t n);
函数解释:将s中前n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
其实这里面的ch就是ascii为ch的字符;
将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值
为什么需要队列
正如以上所说,我们需要一层一层去遍历到所有结点,那么相邻结点的访问顺序如何确定?因此我们就需要一个数据结构去存储和操作,需要使得先遍历到的结点先被存储,直到当前层都被存储之后,按照存储的先后顺序,先被存储的结点也会被先取出来继续遍历他的子节点—>综上所述,需要一种特点为先进先出的数据结构也就是队列! Queue!
queue的特点是: 先进先出(first in first out — FIFO)