[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了.事實應該不是這樣吧….待我再去刷幾道題目先….

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