广度优先搜索基础

一、广搜的特性(队列状态之特性)

当使用BFS遍历一张无权图,每次从队列中取出队首进行一系列扩展,将扩展成功结点放入队尾中;

这样的操作会使得整队列满足“两段性”,即对于这张搜索树中队列只会保留两层的结点;

证明:

  1. 第p层结点扩展时只会扩展第p+1层结点,不会越级扩展p+k层结点(p >= 1&&k >= 2);
  2. 初始的时候队列中只有一层的结点(或者起始点);
  3. 一+二推得:该队列只保留了不超过两层的结点;

由于是“两段性”,而从起始点到达节点所走的最短路程为节点所处的深度,则该队列满足单调性;

综上,对于普通BFS,有两个特性:单调性 、 两段性;

注意:涉及状态时,一定要明确什么是第一关键字,什么是第二关键字。否则,会出现问题!!

二、广度优先搜索的简化代码的技巧

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

如此,在枚举扩展状态时可以预处理几维数组。

三、应用

  • “走地图”类:
例题:Bloxorz

网址:http://poj.org/problem?id=3322

立体推箱子是一个风靡世界的小游戏。

游戏地图是一个N行M列的矩阵,每个位置可能是硬地(用”.”表示)、易碎地面(用”E”表示)、禁地(用”#”表示)、起点(用”X”表示)或终点(用”O”表示)。

你的任务是操作一个1×1×2的长方体。

这个长方体在地面上有两种放置形式,“立”在地面上(1×1的面接触地面)或者“躺”在地面上(1×2的面接触地面)。

在每一步操作中,可以按上下左右四个键之一。

按下按键之后,长方体向对应的方向沿着棱滚动90度。

任意时刻,长方体不能有任何部位接触禁地,并且不能立在易碎地面上。

字符”X”标识长方体的起始位置,地图上可能有一个”X”或者两个相邻的”X”。

地图上唯一的一个字符”O”标识目标位置。

求把长方体移动到目标位置(即立在”O”上)所需要的最少步数。

在移动过程中,”X”和”O”标识的位置都可以看作是硬地被利用。

输入格式

输入包含多组测试用例。

对于每个测试用例,第一行包括两个整数N和M。

接下来N行用来描述地图,每行包括M个字符,每个字符表示一块地面的具体状态。

当输入用例N=0,M=0时,表示输入终止,且该用例无需考虑。

输出格式

每个用例输出一个整数表示所需的最少步数,如果无解则输出”Impossible”。

每个结果占一行。

数据范围

3≤N,M≤500

输入样例:
7 7
#######
#..X###
#..##O#
#....E#
#....E#
#.....#
#######
0 0
输出样例:

10

此题对于初次接触广搜的读者有些困难。

其实,如果把状态定义好,并不是很难。

定义:(x,y,lie)表示该箱子处于(x,y)的位置;lie表示该箱子的状态:

  • 若该箱子立在此处,那么lie=0;
  • 若该箱子向(x,y)右侧“躺下了”,那么lie=1;
  • 若该箱子向(x,y)下方“躺下了”,那么lie=2;

    这样,一个状态定义得十分清晰,其中目标状态为(ex,ey,0);

    为了简化代码,我们使用:

const int next_x[3][4] = {{-2 , 1 , 0 , 0} , {-1 , 1 , 0 , 0} , {-1 , 2 , 0 , 0}};
const int next_y[3][4] = {{0 , 0 , -2 , 1} , {0 , 0 , -1 , 2} , {0 , 0 , -1 , 1}};
const int next_lie[3][4] = {{2 , 2 , 1 , 1} , {1 , 1 , 0 , 0} , {0 , 0 , 2 , 2}};

判断该箱子滚动情况,其中next[ lie ] [ i ]即为当前箱子状态为lie,向方向i滚动情况;

代码如下:

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

using namespace std;
const int MAX_size = 500 + 10;
const int dx[4] = {-1 , 1 , 0 , 0} , dy[4] = {0 , 0 , -1 , 1};//ud -> rl

const int next_x[3][4] = {{-2 , 1 , 0 , 0} , {-1 , 1 , 0 , 0} , {-1 , 2 , 0 , 0}};
const int next_y[3][4] = {{0 , 0 , -2 , 1} , {0 , 0 , -1 , 2} , {0 , 0 , -1 , 1}};
const int next_lie[3][4] = {{2 , 2 , 1 , 1} , {1 , 1 , 0 , 0} , {0 , 0 , 2 , 2}};

struct rec
{
	int x , y , lie;
};

rec st , ed;

int n , m , d[MAX_size][MAX_size][3];
queue <rec> Q;
char map[MAX_size][MAX_size];
void init()
{
	bool valid = true;
	int i , j;
	for(i = 1; i <= n; ++ i)
	{
		for(j = 1; j <= m; ++ j)
		{
			if(map[i][j] == ‘O‘) ed.lie = 0 , ed.x = i , ed.y = j;
			else if(map[i][j] == ‘X‘ && valid)
			{
				valid = false;
				st.x = i , st.y = j , st.lie = false;
				if(map[i + 1][j] == ‘X‘) st.lie = 2;
				if(map[i][j + 1] == ‘X‘) st.lie = 1;
			}
		}
	}

	return;
}

bool valid(int x , int y)
{
	if(x < 1 || x > n || y < 1 || y > m) return false;
	return true;
}
bool valid(int x , int y , int lie)
{
	if(map[x][y] == ‘#‘) return false;
	if(!valid(x , y)) return false;
	if(lie == 0 && map[x][y] == ‘E‘) return false;
	if(lie == 1 && map[x][y + 1] == ‘#‘) return false;
	if(lie == 2 && map[x + 1][y] == ‘#‘) return false;

	return true;
}

int bfs()
{
	int x , y , lie;
	while(!Q.empty()) Q.pop();
	Q.push(rec {st.x , st.y , st.lie});

	d[st.x][st.y][st.lie] = 0;
	while(!Q.empty())
	{
		rec now = Q.front();
		Q.pop();
                // up down left right

		for(int i = 0; i < 4; ++ i)
		{
			x = now.x + next_x[now.lie][i];
			y = now.y + next_y[now.lie][i];
			lie = next_lie[now.lie][i];
			if(d[x][y][lie] != -1) continue;
			if(!valid(x , y , lie)) continue;
			d[x][y][lie] = d[now.x][now.y][now.lie] + 1;
			Q.push(rec {x , y , lie});
			if(x == ed.x && y == ed.y && lie == ed.lie)return d[x][y][lie];
		}
	}
	return -1;
}

int main()
{
	while(scanf("%d %d" , &n , &m) == 2 && n && m)
	{
		memset(d , -1 , sizeof(d));
		memset(map , ‘ ‘ , sizeof(map));
		for(int i = 1; i <= n; ++ i) scanf("%s" , (map[i] + 1) );
		init();
		int ans = bfs();
		if(ans != -1) printf("%d\n" , ans);
		else printf("Impossible\n");
	}
	return 0;
}
练习:武士风度的牛

网址:http://noi-test.zzstep.com/contest/0x29「搜索」练习/2906 武士风度的牛

描述

农民John有很多牛,他想交易其中一头被Don称为The Knight的牛。

这头牛有一个独一无二的超能力,在农场里像Knight一样地跳(就是我们熟悉的象棋中马的走法)。

虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个x,y的坐标图来表示。

这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了The Knight的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。

现在你的任务是,确定The Knight要想吃到草,至少需要跳多少次。

The Knight的位置用’K’来标记,障碍的位置用’*’来标记,草的位置用’H’来标记。

这里有一个地图的例子:

    11 | . . . . . . . . . .
    10 | . . . . * . . . . .
     9 | . . . . . . . . . .
     8 | . . . * . * . . . .
     7 | . . . . . . . * . .
     6 | . . * . . * . . . H
     5 | * . . . . . . . . .
     4 | . . . * . . . * . .
     3 | . K . . . . . . . .
     2 | . . . * . . . . . *
     1 | . . * . . . . * . .
     0 ----------------------
                          1
       0 1 2 3 4 5 6 7 8 9 0

The Knight 可以按照下图中的A,B,C,D…这条路径用5次跳到草的地方(有可能其它路线的长度也是5):

  11 | . . . . . . . . . .
  10 | . . . . * . . . . .
   9 | . . . . . . . . . .
   8 | . . . * . * . . . .
   7 | . . . . . . . * . .
   6 | . . * . . * . . . F<
   5 | * . B . . . . . . .
   4 | . . . * C . . * E .
   3 | .>A . . . . D . . .
   2 | . . . * . . . . . *
   1 | . . * . . . . * . .
   0 ----------------------
                         1
     0 1 2 3 4 5 6 7 8 9 0

注意: 数据保证一定有解。

输入格式

第1行: 两个数,表示农场的列数C(C<=150)和行数R(R<=150)。

第2..R+1行: 每行一个由C个字符组成的字符串,共同描绘出牧场地图。

输出格式

一个整数,表示跳跃的最小次数。

输入样例:
10 11
..........
....*.....
..........
...*.*....
.......*..
..*..*...H
*.........
...*...*..
.K........
...*.....*
..*....*..
输出样例:

5

此题相对于上一道比较简单。

代码 :

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define pa pair <int, int>
using namespace std;
struct rec
{
	int x, y;
} st , ed;

const int MAX_size = 160 + 5;
const int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
const int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

char map[MAX_size][MAX_size];

int n, m;
int d[MAX_size][MAX_size];

bool book[MAX_size][MAX_size];

void init()
{
	memset(d, 0, sizeof(d));
	memset(book, false, sizeof(book));
	for(int i = 1; i <= n; ++ i)
	{
		for(int j = 1; j <= m; ++ j)
		{
			if(map[i][j] == ‘K‘)
			{
				map[i][j] = ‘.‘;
				st.x = i, st.y = j;
				continue;
			}
			if(map[i][j] == ‘H‘)
			{
				map[i][j] = ‘.‘;
				ed.x = i, ed.y = j;
				continue;
			}
		}
	}
	return;
}

bool valid(rec next)
{
	if(next.x < 1 || next.x > n || next.y < 1 || next.y > m) return 0;
	if(map[next.x][next.y] == ‘.‘ && book[next.x][next.y] == false) return 1;
	return 0;
}
int bfs()
{
	queue <rec> Q;
	while(!Q.empty()) Q.pop();

	book[st.x][st.y] = true;
	Q.push(st);

	while(!Q.empty())
	{
		rec now = Q.front(), next;
		Q.pop();
		for(int i = 0; i < 8; ++ i)
		{
			next.x = now.x + dx[i];
			next.y = now.y + dy[i];
			if(valid(next))
			{
				d[next.x][next.y] = d[now.x][now.y] + 1;
				if(next.x == ed.x && next.y == ed.y) return d[ed.x][ed.y];
				book[next.x][next.y] = true;
				Q.push(next);
			}
		}
	}
	return -1;
}
int main()
{
	scanf("%d%d", &m, &n);
	for(int i = 1; i <= n; ++ i) scanf("%s", map[i] + 1);
	init();
	if(st.x == ed.x && st.y == ed.y) puts("0");
	else printf("%d\n", bfs());
	return 0;
}
  • 边界填充---flood-fill问题
例题:矩阵距离

网址:http://noi-test.zzstep.com/contest/0x20「搜索」例题/2501 矩阵距离

给定一个N行M列的01矩阵A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为:

dist(A[i][j],A[k][l])=|i?k|+|j?l|

输出一个N行M列的整数矩阵B,其中:

B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y])

输入格式

第一行两个整数n,m。

接下来一个N行M列的01矩阵,数字之间没有空格。

输出格式

一个N行M列的矩阵B,相邻两个整数之间用一个空格隔开。

数据范围

1≤N,M≤1000

输入样例:
3 4
0001
0011
0110
输出样例:
3 2 1 0
2 1 0 0
1 0 0 1

这就是经典的Flood-fill,就像洒一地水,看能淹了多大的地方。这道题最开始只需将每一个位置为1的坐标放进队列,进行BFS,每次扩展轮数即为该轮数下位置的最短的B[i][j]值。正确性显然。

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<bitset>
#include<queue>
#include<cmath>
#define pii pair <int, int>
using namespace std;

queue <pii> Q;
const int MAX_size = 1000 + 5;
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
bitset <MAX_size> book[MAX_size];

int n, m, map[MAX_size][MAX_size];
int d[MAX_size][MAX_size];
void bfs()
{

	memset(d, 0, sizeof(d));
	while(!Q.empty())
	{
		pii now = Q.front();
		Q.pop();
		int x, y;
		for(int i = 0; i < 4; ++ i)
		{
			x = now.first + dx[i], y = now.second + dy[i];
			if(x < 1 || x > n || y < 1 || y > m) continue;
			if(!book[x][y] && map[x][y] == 0)
			{
				book[x][y] = true;
				d[x][y] = d[now.first][now.second] + 1;
				Q.push(make_pair(x, y));
			}
		}
	}
	return;
}

int main()
{
	while(!Q.empty()) Q.pop();
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; ++ i)
	{
		for(int j = 1; j <= m; ++ j)
		{
			scanf("%1d", &map[i][j]);
			if(map[i][j] == 1) Q.push(make_pair(i, j));
		}
		book[i].reset();
	}

	bfs();
	for(int i = 1; i <= n; ++ i)
	{
		for(int j = 1; j <= m; ++ j) printf("%d ", d[i][j]);
		puts("");
	}
	return 0;
}

练习:乳草的入侵

网址:http://noi-test.zzstep.com/contest/0x29「搜索」练习/2907 乳草的入侵

题意

草地像往常一样,被分割成一个高度为Y, 宽度为X的直角网格。

(1,1)是左下角的格(也就是说坐标排布跟一般的X,Y坐标相同)。

乳草一开始占领了格(Mx,My)。

每个星期,乳草传播到已被乳草占领的格子四面八方的每一个没有很多石头的格(包括垂直与水平相邻的和对角线上相邻的格)内。

1周之后,这些新占领的格又可以把乳草传播到更多的格里面了。

达达想要在草地被乳草完全占领之前尽可能的享用所有的牧草。

她很好奇到底乳草要多久才能占领整个草地。

如果乳草在0时刻处于格(Mx,My),那么几个星期以后它们可以完全占领入侵整片草地呢(对给定的数据总是会发生)?

在草地地图中,”.”表示草,而”*”

表示大石。

比如这个X=4, Y=3的例子。

....
..*.
.**.

如果乳草一开始在左下角(第1排,第1列),那么草地的地图将会以如下态势发展:

      ....  ....  MMM.  MMMM  MMMM
      ..*.  MM*.  MM*.  MM*M  MM*M
      M**.  M**.  M**.  M**.  M**M
星期数  0     1     2     3     4

乳草会在4星期后占领整片土地。

输入格式

第1行: 四个由空格隔开的整数: X, Y, Mx, My

第2到第Y+1行: 每行包含一个由X个字符(”.”表示草地,”*”表示大石)构成的字符串,共同描绘了草地的完整地图。

输出格式

输出一个整数,表示乳草完全占领草地所需要的星期数。

数据范围

1≤X,Y≤100

输入样例:
4 3 1 1
....
..*.
.**.
输出样例:
41

这道题也一样,由此观之,flood-fill实际上就是在一个图上的广度优先搜索之扩展,其算法本质跟广搜差不多。

代码实现:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<cmath>
#define maxn 100 + 5
using namespace std;
struct rec
{
	int x, y;
};

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

int d[maxn][maxn];
char map[maxn][maxn];
int X, Y, Mx, My, cnt = 0;
bool valid(rec next)
{
	if(next.x < 1 || next.x > Y || next.y < 1 || next.y > X) return false;
	if(d[next.x][next.y] != -1) return false;
	return map[next.x][next.y] == ‘.‘;
}
int bfs()
{
	queue <rec> Q;
	while(!Q.empty()) Q.pop();

	d[My][Mx] = 0;
	Q.push(rec {My, Mx});
	int ans = 0;
	while(!Q.empty())
	{
		rec now = Q.front(), next;
		Q.pop();
		for(int i = 0; i < 8; ++ i)
		{
			next.x = now.x + dx[i];
			next.y = now.y + dy[i];
			if(valid(next))
			{
				d[next.x][next.y] = d[now.x][now.y] + 1;
				Q.push(next);
				ans = max(ans, d[next.x][next.y]);
			}
		}
	}
	return ans;
}
int main()
{
	scanf("%d %d %d %d" , &X , &Y , &Mx , &My);
	for(int y = Y; y > 0; -- y)
		scanf("%s", map[y] + 1);

	memset(d, -1, sizeof(d));
	printf("%d\n", bfs());
	return 0;
}

原文地址:https://www.cnblogs.com/zach20040914/p/12629432.html

时间: 2024-10-13 02:17:31

广度优先搜索基础的相关文章

十大基础实用算法之深度优先搜索和广度优先搜索

深度优先搜索算法(Depth-First-Search),是搜索算法的一种.它沿着树的深度遍历树的节点,尽可能深的搜索树的分支.当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点.这一过程一直进行到已发现从源节点可达的所有节点为止.如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止.DFS属于盲目搜索. 深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相

算法与数据结构基础 - 广度优先搜索(BFS)

BFS基础 广度优先搜索(Breadth First Search)用于按离始节点距离.由近到远渐次访问图的节点,可视化BFS 通常使用队列(queue)结构模拟BFS过程,关于queue见:算法与数据结构基础 - 队列(Queue) 最直观的BFS应用是图和树的遍历,其中图常用邻接表或矩阵表示,例如 LeetCode题目 690. Employee Importance: // LeetCode 690. Employee Importance/* class Employee { publi

基础算法(三)——广度优先搜索

广度优先搜索(Breadth First Search),是很多重要的图的算法的原型. 重要的作用:遍历.对于图的遍历,一般有以下的基本思想: ①从图中某个顶点V0出发,并访问此顶点: ②从V0出发,访问V0的各个未曾访问的邻接点W1,W2,-,Wk;然后,依此从W1,W2,-,Wk 出发访问各自未被访问的邻接点. ③重复②,直到全部顶点都被访问为止. [例]如图1-7,按照广度优先搜索的思想遍历这张图. 正确的方法应该是: [例]编写"连连看"的简单程序 规则是:相连不超过两个弯的相

矩阵图中的广度优先搜索

经常会有类似的题目,如迷宫问题,在一个矩阵图中给定出发点和目标点,每次只能上下左右移动,求到目标点的最短走法,或者说是一共有多少种走法. 思路其实很简单,深搜.广搜.相对比较而言,广度优先搜索更加实用于求最短的走法(步数) 在矩阵图中的广搜需要注意一下几点. 1.确定每步的走法:不同题的走法可能不同,每次搜索时将走法保存在数组中. 2.确定初始状态 往往注意刚开始得起点(i,j)必须把MAP(i,j)改变为 -1(或者其他的),栈中第一个元素即为初始状态 3.保存状态.这点很重要.需要用数组或者

广度优先搜索总结

广度优先搜索是对无向图以逻辑上的树的形式从根节点开始进行的逐层遍历. 当题目所求为路径某属性最小的解时适用广度优先搜索,因为如果能使逻辑上的树的层数和所求的最小的属性严格一致,逐层遍历到终点时必然为其属性最小值. 算法实现:基于(优先)队列先进先出的特性,实现优先遍历上层节点,通过标记数组保证访问过的点不被多次访问. 将根节点入队,在while(!q.empty())循环中将队首取出,并建立其子节点信息,如果子节点是合法的就入队并标记,直到找到答案或队列为空表示未找到. 注意问题:1.初始化标记

迷宫问题(maze problem)——深度优先(DFS)与广度优先搜索(BFS)求解

1.问题简介 给定一个迷宫,指明起点和终点,找出从起点出发到终点的有效可行路径,就是迷宫问题(maze problem). 迷宫可以以二维数组来存储表示.0表示通路,1表示障碍.注意这里规定移动可以从上.下.左.右四方方向移动.坐标以行和列表示,均从0开始,给定起点(0,0)和终点(4,4),迷宫表示如下: int maze[5][5]={ {0,0,0,0,0}, {0,1,0,1,0}, {0,1,1,0,0}, {0,1,1,0,1}, {0,0,0,0,0} }; 那么下面的迷宫就有两条

深度优先搜索(DFS)与广度优先搜索(BFS)的Java实现

1.基础部分 在图中实现最基本的操作之一就是搜索从一个指定顶点可以到达哪些顶点,比如从武汉出发的高铁可以到达哪些城市,一些城市可以直达,一些城市不能直达.现在有一份全国高铁模拟图,要从某个城市(顶点)开始,沿着铁轨(边)移动到其他城市(顶点),有两种方法可以用来搜索图:深度优先搜索(DFS)和广度优先搜索(BFS).它们最终都会到达所有连通的顶点,深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现,不同的实现机制导致不同的搜索方式. 1.1 深度优先搜索 深度优先搜索算法有如下规则: 规则1

无向图的深度优先与广度优先搜索代码实现

图采用了邻接表的形式储存. 带不带权都无所谓的 深度优先搜索 Depth First Search 道理和树的先序遍历差不多,把将要访问的点入栈,然后从栈里取点进行访问. 由于这只是类中的一个成员函数,有些被调用的函数的具体代码将会在文章最后补上 ,但是函数功能看注释就好了 1 //深度优先 2 void GraphAdjacencyListWeight::DFSAdvanced(int StartVertex) { 3 int *visited = new int[VertexNumber];

SDUT 2141 【TEST】数据结构实验图论一:基于邻接矩阵的广度优先搜索遍历

数据结构实验图论一:基于邻接矩阵的广度优先搜索遍历 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Discuss Problem Description 给定一个无向连通图,顶点编号从0到n-1,用广度优先搜索(BFS)遍历,输出从某个顶点出发的遍历序列.(同一个结点的同层邻接点,节点编号小的优先遍历) Input 输入第一行为整数n(0< n <100),表示数据的组数.对于每组数据,第一行是三个整数k,m,t(0<