死去元知万事空,但悲不见九州同。
王师北定中原日,家祭无忘告乃翁。——陆游
一、广搜的特性(队列状态之特性)
当使用BFS遍历一张无权图,每次从队列中取出队首进行一系列扩展,将扩展成功结点放入队尾中;
这样的操作会使得整队列满足“两段性”,即对于这张搜索树中队列只会保留两层的结点;
证明:
- 第p层结点扩展时只会扩展第p+1层结点,不会越级扩展p+k层结点(p >= 1&&k >= 2);
- 初始的时候队列中只有一层的结点(或者起始点);
- 一+二推得:该队列只保留了不超过两层的结点;
由于是“两段性”,而从起始点到达节点所走的最短路程为节点所处的深度,则该队列满足单调性;
综上,对于普通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
#include
#include
#include
#include
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 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
#include
#include
#include
#include
#define pa pair
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 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
#include
#include
#include
#include
#include
#define pii pair
using namespace std;
queue Q;
const int MAX_size = 1000 + 5;
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
bitset 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
#include
#include
#include
#include
#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 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;
}