現在我們用堆棧解決一個有意思的問題,定義一個二維數組:
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表示可以走的路,只能橫着走或豎著走,不能斜着走,要求編程序找出從左上角到右下角的路線。程序如下:
例 12.3. 用深度優先搜索解迷宮問題
#include <stdio.h> #define MAX_ROW 5 #define MAX_COL 5 struct point { int row, col; } stack[512]; int top = 0; void push(struct point p) { stack[top++] = p; } struct point 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; }
運行結果如下:
2 1 0 0 0 2 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 0 0 0 0 0 1 1 1 0 0 0 0 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 0 0 0 2 1 1 1 0 0 0 0 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 0 0 0 2 1 1 1 0 2 0 0 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 0 0 0 2 1 1 1 0 2 2 0 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 0 0 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 0 0 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 0 0 0 2 1 0 1 0 2 2 2 0 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 0 0 0 2 1 2 1 0 2 2 2 2 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 0 0 2 1 2 1 0 2 2 2 2 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 2 0 2 1 2 1 0 2 2 2 2 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 2 2 2 1 2 1 0 2 2 2 2 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 2 2 2 1 2 1 2 2 2 2 2 0 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 2 2 2 1 2 1 2 2 2 2 2 2 2 1 1 1 0 2 2 2 1 0 ********* 2 1 2 2 2 2 1 2 1 2 2 2 2 2 2 2 1 1 1 2 2 2 2 1 0 ********* 2 1 2 2 2 2 1 2 1 2 2 2 2 2 2 2 1 1 1 2 2 2 2 1 2 ********* (4, 4) (3, 4) (2, 4) (1, 4) (0, 4) (0, 3) (0, 2) (1, 2) (2, 2) (2, 1) (2, 0) (1, 0) (0, 0)
這次堆棧裡的元素是結構體類型的,用來表示迷宮中一個點的x和y座標。我們用一個新的資料結構保存走迷宮的路線,每個走過的點都有一個前趨(Predecessor)點,表示是從哪兒走到當前點的,比如predecessor[4][4]
是座標為(3, 4)的點,就表示從(3, 4)走到了(4, 4),一開始predecessor
的各元素初始化為無效座標(-1, -1)。在迷宮中探索路線的同時就把路線保存在predecessor
數組中,已經走過的點在maze
數組中記為2防止重複走,最後找到終點時就根據predecessor
數組保存的路線從終點打印到起點。為了幫助理解,我把這個算法改寫成偽代碼(Pseudocode)如下:
將起點標記為已走過並壓棧; while (棧非空) { 從棧頂彈出一個點p; if (p這個點是終點) break; 否則沿右、下、左、上四個方向探索相鄰的點 if (和p相鄰的點有路可走,並且還沒走過) 將相鄰的點標記為已走過並壓棧,它的前趨就是p點; } if (p點是終點) { 打印p點的座標; while (p點有前趨) { p點 = p點的前趨; 打印p點的座標; } } else 沒有路線可以到達終點;
我在while
循環的末尾插了打印語句,每探索一步都打印出當前迷宮的狀態(標記了哪些點),從打印結果可以看出這種搜索算法的特點是:每次探索完各個方向相鄰的點之後,取其中一個相鄰的點走下去,一直走到無路可走了再退回來,取另一個相鄰的點再走下去。這稱為深度優先搜索(DFS,Depth First Search)。探索迷宮和堆棧變化的過程如下圖所示。
圖中各點的編號表示探索順序,堆棧中保存的應該是座標,我在畫圖時為了直觀就把各點的編號寫在堆棧裡了。可見正是堆棧後進先出的性質使這個算法具有了深度優先的特點。如果在探索問題的解時走進了死衚衕,則需要退回來從另一條路繼續探索,這種思想稱為回溯(Backtrack),一個典型的例子是很多編程書上都會講的八皇后問題。
最後我們打印終點的座標並通過predecessor
資料結構找到它的前趨,這樣順藤摸瓜一直打印到起點。那麼能不能從起點到終點正向打印路線呢?在上一節我們看到,數組支持隨機訪問也支持順序訪問,如果在一個循環裡打印數組,既可以正向打印也可以反向打印。但predecessor
這種資料結構卻有很多限制:
可見,有什麼樣的資料結構就決定了可以用什麼樣的算法。那為什麼不再建一個successor
數組來保存每個點的後繼呢?從DFS算法的過程可以看出,雖然每個點的前趨只有一個,後繼卻不止一個,如果我們為每個點只保存一個後繼,則無法保證這個後繼指向正確的路線。由此可見,有什麼樣的算法就決定了可以用什麼樣的資料結構。設計算法和設計資料結構這兩件工作是緊密聯繫的。