[HDOJ 1180]深度优先搜索 vs. 广度优先搜索

题目: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了.事实应该不是这样吧….待我再去刷几道题目先….

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章