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