迷宮問題 - 堆棧與深度優先搜索
堆棧的訪問規則被限制為Push和Pop兩種操作,Push(入棧或壓棧)向棧頂添加元素,Pop(出棧或彈出)則取出當前棧頂的元素,也就是說,只能訪問棧頂元素而不能訪問棧中其它元素。
現在我們用堆棧解決一個有意思的問題,定義一個二維數組:
int maze[5][5] = {
0, 1, 0, 0, 0,
0, 1, 0, 1, 0,
0, 0, 0, 0, 0,
0, 1, 1, 1, 0,
0, 0, 0, 1, 0,
};
它表示一個迷宮,其中的1表示墻壁,0表示可以走的路,只能橫著走或豎著走,不能斜著走,要求編程序找出從左上角到右下角的路線。程序如下:(參考《Linux c 編程一站式學習》)
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#include<stdio.h> typedef struct point { int row, col; } item_t; #define MAX_ROW 5 #define MAX_COL 5 static item_t stack[512]; static int top = 0; void push(item_t p) { stack[top++] = p; } item_t pop(void) { return stack[--top]; } int is_empty(void) { return top == 0; } int maze[MAX_ROW][MAX_COL] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; void print_maze(void) { int i, j; for (i = 0; i < MAX_ROW; i++) { for (j = 0; j < MAX_COL; j++) printf("%d ", maze[i][j]); putchar(‘\n‘); } printf("*********\n"); } struct point predecessor[MAX_ROW][MAX_COL] = { {{ -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}}, {{ -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}}, {{ -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}}, {{ -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}}, {{ -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}, { -1, -1}}, }; void visit(int row, int col, struct point pre) { struct point visit_point = { row, col }; maze[row][col] = 2; predecessor[row][col] = pre; push(visit_point); } int main(void) { struct point p = { 0, 0 }; maze[p.row][p.col] = 2; push(p); while (!is_empty()) { p = pop(); if (p.row == MAX_ROW - 1 /* goal */ && p.col == MAX_COL - 1) break; if (p.col + 1 < MAX_COL /* right */ && maze[p.row][p.col + 1] == 0) visit(p.row, p.col + 1, p); if (p.row + 1 < MAX_ROW /* down */ && maze[p.row + 1][p.col] == 0) visit(p.row + 1, p.col, p); if (p.col - 1 >= 0 /* left */ && maze[p.row][p.col - 1] == 0) visit(p.row, p.col - 1, p); if (p.row - 1 >= 0 /* up */ && maze[p.row - 1][p.col] == 0) visit(p.row - 1, p.col, p); print_maze(); } if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) { printf("(%d, %d)\n", p.row, p.col); while (predecessor[p.row][p.col].row != -1) { p = predecessor[p.row][p.col]; printf("(%d, %d)\n", p.row, p.col); } } else printf("No path!\n"); return 0; } |
輸出為:
這次堆棧裏的元素是結構體類型的,用來表示迷宮中一個點的x和y坐標。我們用一個新的數據結構保存走迷宮的路線,每個走過的點都有一個前趨(Predecessor)點,表示是從哪兒走到當前點的,比如predecessor[4][4]是坐標為(3, 4)的點,就表示從(3, 4)走到了(4, 4),一開始predecessor的各元素初始化為無效坐標(-1,
-1)。在迷宮中探索路線的同時就把路線保存在predecessor數組中,已經走過的點在maze數組中記為2防止重復走,最後找到終點時就根據predecessor數組保存的路線從終點打印到起點。為了幫助理解,把這個算法改寫成偽代碼(Pseudocode)如下圖:
程序在while循環的末尾插了打印語句,每探索一步都打印出當前迷宮的狀態(標記了哪些點),從打印結果可以看出這種搜索算法的特點是:每次探索完各個方向相鄰的點之後,取其中一個相鄰的點走下去,一直走到無路可走了再退回來,取另一個相鄰的點再走下去。這稱為深度優先搜索(DFS,Depth First Search)。探索迷宮和堆棧變化的過程如下圖所示。
圖中各點的編號表示探索順序,堆棧中保存的應該是坐標,在畫圖時為了直觀就把各點的編號寫在堆棧裏了。可見正是堆棧後進先出的性質使這個算法具有了深度優先的特點。如果在探索問題的解時走進了死胡同,則需要退回來從另一條路繼續探索,這種思想稱為回溯(Backtrack),一個典型的例子是很多編程書上都會講的八皇後問題。
最後我們打印終點的坐標並通過predecessor數據結構找到它的前趨,這樣順藤摸瓜一直打印到起點。那麽能不能從起點到終點正向打印路線呢?,數組支持隨機訪問也支持順序訪問,如果在一個循環裏打印數組,既可以正向打印也可以反向打印。但predecessor這種數據結構卻有很多限制:
1. 不能隨機訪問一條路線上的任意點,只能通過一個點找到另一個點,通過另一個點再找第三個點,因此只能順序訪問。
2. 每個點只知道它的前趨是誰,而不知道它的後繼(Successor)是誰,所以只能反向順序訪問。
可見,有什麽樣的數據結構就決定了可以用什麽樣的算法。那為什麽不再建一個successor數組來保存每個點的後繼呢?從DFS算法的過程可以看出,雖然每個點的前趨只有一個,後繼卻不止一個,如果我們為每個點只保存一個後繼,則無法保證這個後繼指向正確的路線。由此可見,有什麽樣的算法就決定了可以用什麽樣的數據結構。設計算法和設計數據結構這兩件工作是緊密聯系的。
參考:《Linux c 編程一站式學習》
迷宮問題 - 堆棧與深度優先搜索