[HDOJ 1180]深度优先搜索 vs. 广度优先搜索
来源:互联网 发布:共产主义社会 知乎 编辑:程序博客网 时间:2024/05/28 11:49
题目: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了.事实应该不是这样吧….待我再去刷几道题目先….
2 0
- [HDOJ 1180]深度优先搜索 vs. 广度优先搜索
- [HDOJ 1181]深度优先搜索 vs. 广度优先搜索 (二)
- 广度 / 深度优先搜索
- 深度优先搜索和广度优先搜索
- 深度优先搜索与广度优先搜索
- 深度优先搜索 广度优先搜索
- 深度优先搜索与广度优先搜索
- 深度优先搜索和广度优先搜索
- 深度优先搜索 广度优先搜索
- 广度优先搜索与深度优先搜索
- 深度优先搜索与广度优先搜索
- 深度优先搜索 广度优先搜索
- 深度优先搜索和广度优先搜索
- 广度优先搜索与深度优先搜索
- 深度优先搜索与广度优先搜索
- 深度优先搜索 广度优先搜索算法
- 深度优先搜索与广度优先搜索
- 深度优先搜索和广度优先搜索
- HDOJ 3336 Count the string
- Logistic回归
- poj 3522
- jquery 实现导航栏滑动效果
- hiho 1290 2016微软4月笔试 dp
- [HDOJ 1180]深度优先搜索 vs. 广度优先搜索
- 打包成jar文件后运行出现Invalid or corrupt jarfile 解决
- poj3617 贪心
- 广度优先搜索
- bower详细教程
- Server Tomcat v7.0 Server at localhost failed to start.
- 【C++标准I/0库】错误:runtime_error was not declared
- 算法之归并排序
- 【湖南集训 4.7】sanrd