题目:http://acm.hdu.edu.cn/showproblem.php?pid=1180
在做这道题目以前,我一直以为能用BFS写的一定也能用DFS写.由于BFS要额外实现一个队列,并且队列中的元素数目呈指数级增长,远不如DFS的递归来的简洁.所以,遇到搜索题目时我总是尽可能使用DFS.确实,有几道BFS的题目被我用DFS AC了,然而,这道题目我用DFS却无论如何也过不了.
事实上,这道题目确实改变了我对BFS的看法.第一,除了要实现一个队列以外,BFS其实要比DFS更加简洁;第二,如果剪枝做到好,BFS使用的内存空间远比理论中的要小.当然,做好剪枝很难.
下面是我的AC代码,GCC编译器,0ms.但是代码很长,而且必须始终小心谨慎地防止内存泄露,这完全是问题之外的工作.
/* 我喜欢把malloc()放在assert()中 */
#include <assert.h>
/* 我需要INT_MAX来充当无穷大 */
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
/* 首先,我实现了一个基于数组的简单队列 */
/*
* 队列
*
* 这是一个基于数组的队列.cell中存放着指向队列元素的指针.
* void**可以保证这个队列适用于任何元素,它是"万能"的.
*
* 当然,你可以像linux那样,用container_of()宏实现一个基于
* 双向链表的万能队列.这样做的好处是你不会大量浪费内存,然而,
* 代价你需要更多的代码和更复杂的逻辑.
*
* 队列总是向高地址处增长.first始终指向队列中的第一个元素,
* last总是指向队列中最后一个元素后面的元素.这样
* 当first == last时,队列是空的;
* 当(last + 1) % size == first时,队列是满的.
*/
struct queue {
void **cell;
int first, last;
int size;
};
/*
* 新建队列
*
* size - 队列大小的估计值,但是你不必担心溢出.
* 后面有专门的代码来处理溢出,这并不复杂.
*/
struct queue *queue_new(int size)
{
struct queue *q;
assert(q = malloc(sizeof(struct queue)));
assert(q->cell = malloc(sizeof(void *) * size));
q->size = size;
q->first = q->last = 0;
return q;
}
/*
* 摧毁队列
*
* 注意,队列中的元素不会被销毁.
* 队列的使用者需要自己把他放进队列中的元素清理干净.
*/
void queue_destroy(struct queue *q)
{
if (q)
free(q->cell);
free(q);
}
/* 重新为队列分配空间 */
void queue_realloc(struct queue *q);
/* 入队 */
void queue_append(void *x, struct queue *q)
{
if ((q->last + 1) % q->size == q->first)
queue_realloc(q);
q->cell[q->last++] = x;
q->last %= q->size;
}
/*
* 当队列满的时候,这个函数重新为队列分配空间.
* 我使用的策略是简单地将队列大小变为当前大小的两倍.
*/
void queue_realloc(struct queue *q)
{
void **cell;
int i, size;
cell = q->cell;
assert(q->cell = malloc(sizeof(void *) * q->size * 2));
for (i = 0, size = q->size - 1; i < size; ++i) {
q->cell[i] = cell[q->first++];
q->first %= q->size;
}
q->first = 0;
q->last = size;
q->size <<= 1;
free(cell);
}
/* 出队 */
void *queue_pop(struct queue *q)
{
void *ret;
ret = q->cell[q->first++];
q->first %= q->size;
return ret;
}
/*
* 至此,队列实现完毕.
* 这个队列与接下来的代码完全是"解耦合"的.
*/
/*
* 节点
*
* deepth域其实没被使用.但它却被写进了结构体中,
* 这是我对问题理解不深刻导致的.
*/
struct node {
int x, y;
int deepth;
};
/*
* 为一个新的节点分配内存,并返回这个节点
*/
struct node *node_new(int x, int y)
{
struct node *n;
assert(n = malloc(sizeof(struct node)));
n->x = x;
n->y = y;
return n;
}
/*
* 销毁一个节点.
* 我们只是简单的调用free()函数
*/
void node_destroy(struct node *n)
{
free(n);
}
/*
* 销毁整个队列,包括队列中的元素以及队列本身
*/
void node_queue_destroy(struct queue *q)
{
while (q->first != q->last) {
node_destroy(q->cell[q->first++]);
q->first %= q->size;
}
queue_destroy(q);
}
/* 这是地图的长和宽 */
int height, width;
/* 两张地图,map_0是原始地图;当步长变成奇数的时候,使用map_odd */
char map_0[20][20], map_odd[20][20];
/* 方向,x += dir[d], y += dir[d+5] */
char dir[10] = {0, 1, -1, 0, 0, 0, 0, 0, 1, -1};
/*
* 通常,我们将visited数组定义成二维的:
*
* int visited[20][20];
*
* 在二维的visited数组中,当节点(x,y)从未被访问过时
* visited[x][y]的值是无穷大(INT_MAX).
* 当我们第一次访问它时,它的有了一个肯定比无穷大小的值,
* 于是我们用这个小的值替换掉无穷大.
* 当我们再一次经过(x,y)的时候,除非当前的路径长度比visited[x][y]
* 的值小(这在BFS里面几乎肯定不会发生),否则我们不会更新visited[x][y]
*
* 但是,就这道题目而言,有时候我们却不得不需要用一个更大的值
* 来更新visited[x][y].
*
* 请看这幅地图:
* S|T
* 我们第一次经过S时,visited[S]的值是0,但是,
* 在我们经由S到达T的最终路径上,
* visited[S]的值却应该是1,因为我们在S上停留了一次.
*
* 造成这种局面的原因是,我们的地图是在不断变化的,
* 地图有两种状态,因此,地图上的每一个节点也随之
* 获得了两种状态.在同一个节点的不同状态之间比较
*
* 因此我给了visited数组第三个纬度,
* 代表一个节点的两种状态.相应的,
* 我更新visited数组的策略也从"总是用最小的路径
* 长度更新节点(x,y)在visited中的值"变成"总是用最小
* 路径长度更新处于同一状态的(x,y)在visited中的值".
*/
int visited[20][20][2];
/* 广度优先搜索,返回从S到T的最短路径 */
int bfs(int start_x, int start_y)
{
int deepth = 0;
/* 这个变量用来处理内存泄漏 */
struct node *last_c = NULL;
int x, y, d;
char (*map)[20], content;
struct node *n, *c;
struct queue *q;
/* 初始化visited数组 */
for (x = 0; x < height; ++x)
for (y = 0; y < width; ++y)
visited[x][y][0] = visited[x][y][1] = INT_MAX;
n = node_new(start_x, start_y);
n->deepth = 0;
/*
* 我没有计算q的可能大小,而是非常
* 不负责任地随便给了它一个初始大小:1.
* 这使得这个队列在一开始的时候就是满的.
*/
q = queue_new(1);
/* 插入第一个节点,驱动广度优先搜索 */
queue_append(n, q);
visited[start_x][start_y][0] = 0;
/*
* 每当深度增加地时候,
* 都会goto到这里来.
* 按照Dijkstra前辈观点,
* 我似乎不应该使用goto语句和这个标签,
* 因为我只要把dive中地语句嵌套进一个循环中就好了.
* 但是,我觉得那样会使代码更加复杂.
*/
dive:
/* 根据当前地深度,更新我们所用地地图 */
map = ((deepth & 1UL) ? map_odd : map_0);
/* 把NULL排在当前层的尾巴上,作为层结束地标志 */
queue_append(NULL, q);
while ((c = (struct node*)queue_pop(q))) {
/* 小心谨慎地处理内存泄漏 */
node_destroy(last_c);
last_c = c;
/*
* 由于考虑不周,node地deepth域其实是多余的.
*/
/* visited[c->x][c->y][deepth & 1UL] = c->deepth; */
/* 遍历5个方向:上,下,左,右,不动 */
for (d = 0; d < 5; ++d) {
x = c->x + dir[d];
y = c->y + dir[d + 5];
/* 当我们踩到楼梯上时,
* 会向前滑一步,这时,
* 我们必须重新对
* 当前节点进行合法性以及合理性判断.
* 与上一个dive标签不同,这个slide
* 标签我自以为用的挺合理:P
*/
slide:
if (x < 0 || x >= height || y < 0 || y >= width)
continue;
content = map[x][y];
if (content == 'T') {
node_queue_destroy(q);
node_destroy(c);
return ++deepth;
}
if (content == '*'
|| (content == '|' && dir[d + 5])
|| (content == '-' && dir[d]))
continue;
/* 踩到楼梯上 */
if ((content == '|' && dir[d])
|| content == '-' && dir[d + 5]) {
x += dir[d];
y += dir[d + 5];
goto slide;
}
/*
* 经过了重重检验以后,我终于可以确定自己踩
* 在了一个合理地位置上.现在,我要检验一下
* 我地这次来访是不是比我之前地来访更加
* "优秀",也就是拥有更短的路径长度.
*/
if (visited[x][y][(deepth + 1) & 1UL] <= deepth + 1)
continue;
/*
* 我的到来是开拓性的,现在我要
* 被载入史册了.
*/
n = node_new(x, y);
queue_append(n, q);
/*
* 我在记得dijkstra算法中,
* 节点标记的更新是在节点从
* 队列中取出的时候.因此,
* 我一开始的时候不假思索地将
* 这段代码写在了while循环地开头,
* 结果是永远都不够用地内存.
*
* 其实,我只记对了一半,
* 标记地更新的确是在出队地时候,
* 而距离地更新却是在入队的时候.
*
* 这里的关键思想是:无论何时,一旦一个
* 节点有了更短的路径长度,它就应该
* 立即被记录下来.记录的时刻越早,
* 就能阻止越多不知情的相同节点盲目入队.
*/
visited[x][y][(deepth + 1) & 1UL] = deepth + 1;
}
}
/* 当我们结束了一层以后,就做一些简单处理 */
if (q->last != q->first) {
++deepth;
goto dive;
}
}
int main(void)
{
int x, y, start_x, start_y;
freopen("Inputs/1180", "r", stdin);
setbuf(stdout, NULL);
while (scanf("%d%d%*c", &height, &width) != EOF) {
for (x = 0; x < height; ++x) {
for (y = 0; y < width; ++y) {
scanf("%c", &map_0[x][y]);
if (map_0[x][y] == 'S') {
start_x = x;
start_y = y;
}
}
scanf("%*c");
}
for (x = 0; x < height; ++x) {
for (y = 0; y < width; ++y) {
if (map_0[x][y] == '|')
map_odd[x][y] = '-';
else if(map_0[x][y] == '-')
map_odd[x][y] = '|';
else
map_odd[x][y] = map_0[x][y];
}
}
printf("%d\n", bfs(start_x, start_y));
}
return 0;
}
为什么在这里BFS要优于DFS?
对于任意一个节点X,它一旦被我们发现,它在当前状态下的最优路径也就同时被我们发现了.这样,在未来的任意时刻,只要我们再次碰到这个节点(相同状态下),我们就可以立即把它过滤(剪枝)掉.这是BFS优于DFS的最关键的地方.在DFS中,我们无论已经经过了同一个节点多少次,我们都不敢扬言说已经找到了到这个节点的最短路径.因此我们可能会在同一个节点上反反复复走很多很多次.
在一个20X20的地图上,基于以下两个事实:1.一个节点最多只有两个状态;2.经过一次的节点绝不会再经过第二次.我们可以知道,我们最多只要遍历800个节点.
这样看来,似乎又是BFS完胜DFS了.事实应该不是这样吧….待我再去刷几道题目先….