[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
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 三岁宝宝智商低怎么办 宝宝断奶晚上哭的厉害怎么办 2岁宝宝半夜喝奶粉怎么办 两岁宝宝不爱吃饭怎么办 快两岁的宝宝不爱吃饭怎么办 宝宝断奶后不愿意喝奶粉怎么办 宝宝断奶了不愿意喝奶怎么办? 宝宝断奶不愿意喝奶粉怎么办 宝宝断奶不愿意喝牛奶怎么办? 四个月宝宝断奶不吃奶粉怎么办 2岁不开口说话怎么办 八个月宝宝断奶不吃奶粉怎么办 宝宝断奶不喝奶粉怎么办 周岁 给娘家东西婆家看见怎么办 自己娘家妈总说婆家人坏话怎么办 娘家婆家老公都没有依靠怎么办? 2岁宝宝断奶粉怎么办 2岁宝宝夜奶频繁怎么办 宝宝15个月还在吃夜奶怎么办 宝宝两岁四个月还吃母乳怎么办 宝宝睡前老是找奶吃怎么办 宝宝戒奶晚上哭怎么办 宝宝戒奶半夜哭怎么办 吸习惯母乳不吸奶嘴怎么办 八个月宝宝奶睡怎么办 宝宝要吸着奶睡怎么办 戒母乳胸胀的疼怎么办 断奶孩子晚上哭的厉害怎么办 喜欢咬指甲的人怎么办? 成年了还咬指甲怎么办 戒奶乳房有硬块怎么办 戒奶七天有硬块怎么办 两岁宝宝喘气粗怎么办 两岁宝宝断不了奶怎么办 两岁宝宝不愿意喝奶粉怎么办 吃母乳不愿意吃奶粉怎么办 母乳宝宝不愿意喝奶粉怎么办 宝宝断母乳不喝奶粉怎么办 9个月宝宝不会爬怎么办 孩子五年级学习成绩差该怎么办 孩子临近中考学习成绩很差该怎么办