BFS算法模板与练习

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BFS算法模板与练习相关的知识,希望对你有一定的参考价值。

文章和代码已经归档至【Github仓库:​​algorithms-notes​​】或者公众号【AIShareLab】回复 算法笔记 也可获取。

首先,计算机中常用的数据结构是栈和队列。

  • 栈:先进后出,通常应用是递归,DFS。
  • 队列:先进先出,通常应用是 BFS 。

过程如下所示:

每次取出队头元素,并且把其拓展的元素放在队尾。

BFS算法模板与练习_数据

上面过程可知,遍历的过程以及入队的过程都是按照BFS(1 2 3...10)的顺序进行的

BFS宽搜:每次扩展最早的点。(因此可以找到一条最短的路径)

DFS深搜:每次扩展第一个点。

BFS中常见问题,迷宫问题。

模板

1.判重

入队时判重,保证每个边只会入队一次,从而保证时间复杂度是线性的。(因此有判重数组的存在,宽搜也可以搜索环),st[ ]。

2.队列

queue <--- 初始状态 // 队列保存初始状态
while(queue 非空)

t < --- 队头 // t保存队头
for(拓展t)

ver < --- 新节点 // 拓展t得到的新节点
if(!st[ver]) // 如果拓展的新节点没有被搜索过

ver ----> 队尾 // 保存至队尾


献给阿尔吉侬的花束

阿尔吉侬是一只聪明又慵懒的小白鼠,它最擅长的就是走各种各样的迷宫。

今天它要挑战一个非常大的迷宫,研究员们为了鼓励阿尔吉侬尽快到达终点,就在终点放了一块阿尔吉侬最喜欢的奶酪。

现在研究员们想知道,如果阿尔吉侬足够聪明,它最少需要多少时间就能吃到奶酪。

迷宫用一个 R×C 的字符矩阵来表示。

字符 S 表示阿尔吉侬所在的位置,字符 E 表示奶酪所在的位置,字符 # 表示墙壁,字符 . 表示可以通行。

阿尔吉侬在 1 个单位时间内可以从当前的位置走到它上下左右四个方向上的任意一个位置,但不能走出地图边界。

输入格式

第一行是一个正整数 T,表示一共有 T 组数据。

每一组数据的第一行包含了两个用空格分开的正整数 R 和 C,表示地图是一个 R×C 的矩阵。

接下来的 R 行描述了地图的具体内容,每一行包含了 C 个字符。字符含义如题目描述中所述。保证有且仅有一个 S 和 E。

输出格式

对于每一组数据,输出阿尔吉侬吃到奶酪的最少单位时间。

若阿尔吉侬无法吃到奶酪,则输出“oop!”(只输出引号里面的内容,不输出引号)。

每组数据的输出结果占一行。

数据范围

BFS算法模板与练习_BFS_02

,

BFS算法模板与练习_BFS_03

输入样例:

3
3 4
.S..
###.
..E.
3 4
.S..
.E..
....
3 4
.S..
####
..E.

输出样例:

5
1
oop!

code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

#define x first
#define y second

using namespace std;
// pair 有两个属性,first 和 second ,建议宏定义为x和y,方便理解
typedef pair<int, int> PII;

const int N = 210;

int n, m;
// 存储地图
char g[N][N];
// 存储坐标
int dist[N][N];

int bfs(PII start, PII end)

queue<PII> q;
memset(dist, -1, sizeof dist);

dist[start.x][start.y] = 0;
q.push(start);

// 存储坐标位移
int dx[4] = -1, 0, 1, 0, dy[4] = 0, 1, 0, -1;

while (q.size())

auto t = q.front();
q.pop();
// 遍历坐标
for (int i = 0; i < 4; i ++ )

int x = t.x + dx[i], y = t.y + dy[i];
if (x < 0 || x >= n || y < 0 || y >= m) continue; // 出界
if (g[x][y] == #) continue; // 障碍物
if (dist[x][y] != -1) continue; // 之前已经遍历过

dist[x][y] = dist[t.x][t.y] + 1;

if (end == make_pair(x, y)) return dist[x][y];

q.push(x, y);


return -1;


int main()

int T;
scanf("%d", &T);
while (T -- )

scanf("%d%d", &n, &m);
// 一共读取n行,每行是一个一维字符串
for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);
// 不用定义成全局变量,如果定义全局变量会造成关键字冲突。
// 如果想定义为全局变量,可以考虑换个名称例如 end1 等。
PII start, end;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (g[i][j] == S) start = i, j;
else if (g[i][j] == E) end = i, j;

int distance = bfs(start, end);
if (distance == -1) puts("oop!");
else printf("%d\\n", distance);


return 0;

交换瓶子

有 N 个瓶子,编号 1∼N,放在架子上。

比如有 5 个瓶子:

2 1 3 5 4

要求每次拿起 2 个瓶子,交换它们的位置。

经过若干次后,使得瓶子的序号为:

1 2 3 4 5

对于这么简单的情况,显然,至少需要交换 2 次就可以复位。

如果瓶子更多呢?你可以通过编程来解决。

输入格式

第一行包含一个整数 N,表示瓶子数量。

第二行包含 N 个整数,表示瓶子目前的排列状况。

输出格式

输出一个正整数,表示至少交换多少次,才能完成排序。

数据范围

BFS算法模板与练习_BFS_04

输入样例1:

5
3 1 2 5 4

输出样例1:

3

输入样例2:

5
5 4 3 2 1

输出样例2:

2

暴力思路

这道题可以采用暴力思路,通过观察可以发现,我们每一个数都必须回到它自己的位置上,比如 1 必须在第一位,2 必须在第二位上。由于每个数必须回到自己的位置,直接从 1 枚举到 n,如果当前位置的数不等于它的下标,那么我们就必须要把它给替换掉。设当前位置为 i 的话,那么我们就从 i+1开始往后枚举,直到找到对应的 a[j] 和我们的 i 相等,那么我们就把上个数交换,把交换次数++。由于每个瓶子都要归位,因此不会出现多余的步骤,可知是最少的次数。

code

#include <iostream>
#include <cstring>

using namespace std;

const int N = 10010;
int a[N], sum;

int main()

int n;
cin >> n;

for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

for(int i = 1; i <= n; i ++ )
if(a[i] != i)
for(int j = i + 1; j <= n; j ++ )
if(a[j] == i)

swap(a[j], a[i]);
sum ++ ;
break;

cout << sum << endl;
return 0;

BFS图论思路

初始状态如下所示:

BFS算法模板与练习_数据_05

其中,每个位置向所在的瓶子连一条有向线。

BFS算法模板与练习_数据_06

出度是1,入度是1,这样的一个环称为置换。

BFS算法模板与练习_数据_07

最终希望的状态是变成五个自环。

交换两个瓶子对于环产生的影响

  1. 交换同一个环内的点:裂成两个环。

BFS算法模板与练习_BFS_08

  1. 交换不同环内的点:合并两个环。

BFS算法模板与练习_BFS_09

可见,交换瓶子实际上改变了位置连向瓶子的出边,也就是瓶子的入边。

分析题意,初始的时候有k个环,要将其变为n个环,每次操作最多增加一个环,因此最少需要 n - k 次操作才能完成。

算法复杂度为

BFS算法模板与练习_数据_10

,因为每个点被遍历常数次。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010;

int n;
// 瓶子的数量
int b[N];
// 判重数组帮助找环
bool st[N];

int main()

scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);

int cnt = 0;
for (int i = 1; i <= n; i ++ )
if (!st[i])
// 当前环没有被找过,说明在一个新环

cnt ++ ;
// 把这个点能到达的点都标记一下
for (int j = i; !st[j]; j = b[j])
st[j] = true;


printf("%d\\n", n - cnt);

return 0;

BFS练习(附带hash算法)

这个代码真的看的好久,不是我自己写的,但是陌生的东西太多了,孩怕啊。




#include<cstdio>

#include<cstring>

#include<set>

using namespace std; 

typedef int State[9];//定义“状态类型” 

const int maxstate=1000000; 

State st[maxstate],goal;//状态数组。所有状态都保存在这里 

int dist[maxstate]; //距离数组


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

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

//BFS,返回目标状态在st数组下标

//编码与解码 

set<int> vis;

void init_lookup_table()

{

vis.clear();

}

int try_to_insert(int s)

{

int v=0;

for(int i=0;i<9;i++)

v=v*10+st[s][i];

if(vis.count(v)) return 0;

vis.insert(v);

return 1;

}

int bfs()

{

init_lookup_table(); //初始化查找表

int front=1,rear=2;//不使用下标0,因为0被看作"不存在",front为队首指针,rear为队尾指针,因为要符合前闭后开的习惯 

while(front<rear)//队列不为空 

{

State& s=st[front]; //用“引用”来简化代码

if(memcmp(goal,s,sizeof(s))==0) return front;//找到目标状态,成功返回

int z;

for(z=0;z<9;z++) if(!s[z]) break;

int x=z/3,y=z%3; //获取行列编号

for(int d=0;d<4;d++)

{

int newx=x+dx[d];

int newy=y+dy[d];

int newz=newx*3+newy;

if(newx>=0&&newy<3&&newy>=0&&newx<3)//若移动合法 

{

State& t=st[rear]; //扩展新结点 

memcpy(&t,&s,sizeof(s));

t[newz]=s[z];

t[z]=s[newz];//交换移动的元素 

dist[rear]=dist[front]+1;//更新结点的距离值

if(try_to_insert(rear)) rear++;//如果成功插入查找表,修改队尾指针 

}

}

front++;

}

return 0;

}

int main()

{

for(int i=0;i<9;i++)

scanf("%d",&st[1][i]);

for(int i=0;i<9;i++)

scanf("%d",&goal[i]);

int ans=bfs();

if(ans>0) printf("%d",dist[ans]);

else printf("-1\n");

return 0;

}

/*

编码与解码的另两种写法 

1.

int vis[362880],fact[9];

void init_lookup_table()

{

fact[0]=1;

for(int i=1;i<9;i++)

fact[i]=fact[i-1]*i;

}

int try_to_insert(int s)

{

int code=0;//st[s]映射到整数code

for(int i=0;i<9;i++)

{

int cnt;

for(int j=i+1;j<9;j++)

if(st[s][j]<st[s][i]) cnt++;

code+=fact[8-i]*cnt;

}

if(vis[code]) return 0;

return vis[code]=1;

}

2.hash技术

const int hashsize=1000003;

int head[hashsize],next[maxstate]={0};

void init_lookup_table()

{

memset(head,0,sizeof(head));

}

int hash(State &s)

{

int v=0;

for(int i=0;i<9;i++)

v=v*10+s[i];//把九个数字组成九位数 

return v%hashsize;//确保hash函数值是不超过hash表的大小的非负整数 

}

int try_to_insert(int s)

{

int h=hash(st[s]);//h记录编码值 

int u=head[h];

while(u)//若u不重复 

{

if(memcmp(st[u],st[s],sizeof(st[s]))==0)

return 0;

u=next[u];

}

next[s]=head[h];

head[h]=s;

return 1;

}

*/



大概说一下几个要点

  1. dx,dy两个数组,分别模拟四个方向的走动,这个要好好学习,我记得之前我好像是用一堆堆判断来写的,实在蠢死了。

  2. 作者是用数组来模拟队列,队列模拟需要一个线性表和一个头指针,一个尾指针。

  3. hash算法:先将一段信息通过编码(这个必须是个双射)得到一个大整数,然后对大整数取模来缩小这个数据,此时有可能获得多段信息hash值相等,此时将hash值相等的所有信息连成链表,当遇到hash值相等的信息时,直接比较本来的信息,否则直接比较hash值即可判重。

  4. 还有引用的用法,这里不详细解释了




当你debug

以上是关于BFS算法模板与练习的主要内容,如果未能解决你的问题,请参考以下文章

Dijkstra 算法复杂度与 BFS 复杂度

BFS与DFS模板总结

BFS与DFS模板总结

《算法竞赛进阶指南》0x25广度优先搜索 推箱子游戏 双重BFS

BFS算法模板(python实现)

BFS练习(附带hash算法)