1. 程式人生 > >迷宮問題 - 堆棧與深度優先搜索

迷宮問題 - 堆棧與深度優先搜索

tchar ace 通過 二維 皇後 printf ali 不知道 body

堆棧的訪問規則被限制為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 編程一站式學習》

迷宮問題 - 堆棧與深度優先搜索