深度優先搜尋遍歷與廣度優先搜尋遍歷
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
深度優先遍歷過程
1、圖的遍歷
和樹的遍歷類似,圖的遍歷也是從某個頂點出發,沿著某條搜尋路徑對圖中每個頂點各做一次且僅做一次訪問。它是許多圖的演算法的基礎。
深度優先遍歷和廣度優先遍歷是最為重要的兩種遍歷圖的方法。它們對無向圖和有向圖均適用。
注意:
以下假定遍歷過程中訪問頂點的操作是簡單地輸出頂點。
2、布林向量visited[0..n-1]的設定
圖中任一頂點都可能和其它頂點相鄰接。在訪問了某頂點之後,又可能順著某條迴路又回到了該頂點。為了避免重複訪問同一個頂點,必須記住每個已訪問的頂點。為此,可設一布林向量visited[0..n-1],其初值為假,一旦訪問了頂點Vi之後,便將visited[i]置為真。
深度優先遍歷(Depth-First Traversal)
1.圖的深度優先遍歷的遞迴定義
假設給定圖G的初態是所有頂點均未曾訪問過。在G中任選一頂點v為初始出發點(源點),則深度優先遍歷可定義如下:首先訪問出發點v,並將其標記為已訪問過;然後依次從v出發搜尋v的每個鄰接點w。若w未曾訪問過,則以w為新的出發點繼續進行深度優先遍歷,直至圖中所有和源點v有路徑相通的頂點(亦稱為從源點可達的頂點)均已被訪問為止。若此時圖中仍有未訪問的頂點,則另選一個尚未訪問的頂點作為新的源點重複上述過程,直至圖中所有頂點均已被訪問為止。
圖的深度優先遍歷類似於樹的前序遍歷。採用的搜尋方法的特點是儘可能先對縱深方向進行搜尋。這種搜尋方法稱為深度優先搜尋(Depth-First Search)。相應地,用此方法遍歷圖就很自然地稱之為圖的深度優先遍歷。
2、深度優先搜尋的過程
設x是當前被訪問頂點,在對x做過訪問標記後,選擇一條從x出發的未檢測過的邊(x,y)。若發現頂點y已訪問過,則重新選擇另一條從x出發的未檢測過的邊,否則沿邊(x,y)到達未曾訪問過的y,對y訪問並將其標記為已訪問過;然後從y開始搜尋,直到搜尋完從y出發的所有路徑,即訪問完所有從y出發可達的頂點之後,才回溯到頂點x,並且再選擇一條從x出發的未檢測過的邊。上述過程直至從x出發的所有邊都已檢測過為止。此時,若x不是源點,則回溯到在x之前被訪問過的頂點;否則圖中所有和源點有路徑相通的頂點(即從源點可達的所有頂點)都已被訪問過,若圖G是連通圖,則遍歷過程結束,否則繼續選擇一個尚未被訪問的頂點作為新源點,進行新的搜尋過程。
3、深度優先遍歷的遞迴演算法
(1)深度優先遍歷演算法
typedef enum{FALSE,TRUE}Boolean;//FALSE為0,TRUE為1
Boolean visited[MaxVertexNum]; //訪問標誌向量是全域性量
void DFSTraverse(ALGraph *G)
{ //深度優先遍歷以鄰接表表示的圖G,而以鄰接矩陣表示G時,演算法完全與此相同
int i;
for(i=0;i<G->n;i++)
visited[i]=FALSE; //標誌向量初始化
for(i=0;i<G->n;i++)
if(!visited[i]) //vi未訪問過
DFS(G,i); //以vi為源點開始DFS搜尋
}//DFSTraverse
(2)鄰接表表示的深度優先搜尋演算法
void DFS(ALGraph *G,int i){
//以vi為出發點對鄰接表表示的圖G進行深度優先搜尋
EdgeNode *p;
printf("visit vertex:%c",G->adjlist[i].vertex);//訪問頂點vi
visited[i]=TRUE; //標記vi已訪問
p=G->adjlist[i].firstedge; //取vi邊表的頭指標
while(p){//依次搜尋vi的鄰接點vj,這裡j=p->adjvex
if (!visited[p->adjvex])//若vi尚未被訪問
DFS(G,p->adjvex);//則以Vj為出發點向縱深搜尋
p=p->next; //找vi的下一鄰接點
}
}//DFS
(3)鄰接矩陣表示的深度優先搜尋演算法
void DFSM(MGraph *G,int i)
{ //以vi為出發點對鄰接矩陣表示的圖G進行DFS搜尋,設鄰接矩陣是0,l矩陣
int j;
printf("visit vertex:%c",G->vexs[i]);//訪問頂點vi
visited[i]=TRUE;
for(j=0;j<G->n;j++) //依次搜尋vi的鄰接點
if(G->edges[i][j]==1&&!visited[j])
DFSM(G,j)//(vi,vj)∈E,且vj未訪問過,故vj為新出發點
}//DFSM
注意:
遍歷操作不會修改圖G的內容,故上述演算法中可將G定義為ALGraph或MGraph型別的引數,而不一定要定義為ALGraph和MGraph的指標型別。但基於效率上的考慮,選擇指標型別的引數為宜。
4、深度優先遍歷序列
對圖進行深度優先遍歷時,按訪問頂點的先後次序得到的頂點序列稱為該圖的深度優先遍歷序列,或簡稱為DFS序列。
(1)一個圖的DFS序列不一定惟一
當從某頂點x出發搜尋時,若x的鄰接點有多個尚未訪問過,則我們可任選一個訪問之。
(2)源點和儲存結構的內容均已確定的圖的DFS序列惟一
① 鄰接矩陣表示的圖確定源點後,DFS序列惟一
DFSM演算法中,當從vi出發搜尋時,是在鄰接矩陣的第i行上從左至右選擇下一個未曾訪問過的鄰接點作為新的出發點,若這樣的鄰接點多於一個,則選中的總是序號較小的那一個。
②只有給出了鄰接表的內容及初始出發點,才能惟一確定其DFS序列
鄰接表作為給定圖的儲存結構時,其表示不惟一。因為鄰接表上邊表裡的鄰接點域的內容與建表時的輸入次序相關。
因此,只有給出了鄰接表的內容及初始出發點,才能惟一確定其DFS序列。
3)棧在深度優先遍歷演算法中的作用
深度優先遍歷過程中,後訪問的頂點其鄰接點被先訪問,故在遞迴呼叫過程中使用棧(系統執行時刻棧)來儲存已訪問的頂點。
5、演算法分析
對於具有n個頂點和e條邊的無向圖或有向圖,遍歷演算法DFSTraverse對圖中每頂點至多呼叫一次DFS或DFSM。從DFSTraverse中呼叫DFS(或DFSM)及DFS(或DFSM)內部遞迴呼叫自己的總次數為n。
當訪問某頂點vi時,DFS(或DFSM)的時間主要耗費在從該頂點出發搜尋它的所有鄰接點上。用鄰接矩陣表示圖時,其搜尋時間為O(n);用鄰接表表示圖時,需搜尋第i個邊表上的所有結點。因此,對所有n個頂點訪問,在鄰接矩陣上共需檢查n2個矩陣元素,在鄰接表上需將邊表中所有O(e)個結點檢查一遍。
所以,DFSTraverse的時間複雜度為O(n2) (呼叫DFSM)或0(n+e)(呼叫DFS)。
廣度優先遍歷過程
1、廣度優先遍歷的遞迴定義
設圖G的初態是所有頂點均未訪問過。在G中任選一頂點v為源點,則廣度優先遍歷可以定義為:首先訪問出發點v,接著依次訪問v的所有鄰接點w1,w2,…,wt,然後再依次訪問與wl,w2,…,wt鄰接的所有未曾訪問過的頂點。依此類推,直至圖中所有和源點v有路徑相通的頂點都已訪問到為止。此時從v開始的搜尋過程結束。
若G是連通圖,則遍歷完成;否則,在圖C中另選一個尚未訪問的頂點作為新源點繼續上述的搜尋過程,直至G中所有頂點均已被訪問為止。
廣度優先遍歷類似於樹的按層次遍歷。採用的搜尋方法的特點是儘可能先對橫向進行搜尋,故稱其為廣度優先搜尋(Breadth-FirstSearch)。相應的遍歷也就自然地稱為廣度優先遍歷。
2、廣度優先搜尋過程
在廣度優先搜尋過程中,設x和y是兩個相繼要被訪問的未訪問過的頂點。它們的鄰接點分別記為x1,x2,…,xs和y1,y2,…,yt。
為確保先訪問的頂點其鄰接點亦先被訪問,在搜尋過程中使用FIFO佇列來儲存已訪問過的頂點。當訪問x和y時,這兩個頂點相繼入隊。此後,當x和y相繼出隊時,我們分別從x和y出發搜尋其鄰接點x1,x2,…,xs和y1,y2,…,yt,對其中未訪者進行訪問並將其人隊。這種方法是將每個已訪問的頂點人隊,故保證了每個頂點至多隻有一次人隊。
3、廣度優先搜尋演算法
(1)鄰接表表示圖的廣度優先搜尋演算法
void BFS(ALGraph*G,int k)
{// 以vk為源點對用鄰接表表示的圖G進行廣度優先搜尋
int i;
CirQueue Q; //須將佇列定義中DataType改為int
EdgeNode *p;
InitQueue(&Q);//佇列初始化
//訪問源點vk
printf("visit vertex:%e",G->adjlist[k].vertex);
visited[k]=TRUE;
EnQueue(&Q,k);//vk已訪問,將其人隊。(實際上是將其序號人隊)
while(!QueueEmpty(&Q)){//隊非空則執行
i=DeQueue(&Q); //相當於vi出隊
p=G->adjlist[i].firstedge; //取vi的邊表頭指標
while(p){//依次搜尋vi的鄰接點vj(令p->adjvex=j)
if(!visited[p->adivex]){ //若vj未訪問過
printf("visitvertex:%c",C->adjlistlp->adjvex].vertex); //訪問vj
visited[p->adjvex]=TRUE;
EnQueue(&Q,p->adjvex);//訪問過的vj人隊
}//endif
p=p->next;//找vi的下一鄰接點
}//endwhile
}//endwhile
}//end of BFS
(2)鄰接矩陣表示的圖的廣度優先搜尋演算法
void BFSM(MGraph *G,int k)
{以vk為源點對用鄰接矩陣表示的圖G進行廣度優先搜尋
int i,j;
CirQueue Q;
InitQueue(&Q);
printf("visit vertex:%c",G->vexs[k]); //訪問源點vk
visited[k]=TRUE;
EnQueue(&Q,k);
while(!QueueEmpty(&Q)){
i=DeQueue(&Q); //vi出隊
for(j=0;j<G->n;j++)//依次搜尋vi的鄰接點vj
if(G->edges[i][j]==1&&!visited[j]){//vi未訪問
printf("visit vertex:%c",G->vexs[j]);//訪問vi
visited[j]=TRUE;
EnQueue(&Q,j);//訪問過的vi人隊
}
}//endwhile
}//BFSM
(3)廣度優先遍歷演算法
類似於DFSTraverse。【參見DFSTraverse演算法】
4、圖的廣度優先遍歷序列
廣度優先遍歷圖所得的頂點序列,定義為圖的廣度優先遍歷序列,簡稱BFS序列。
(1)一個圖的BFS序列不是惟一的
(2)給定了源點及圖的儲存結構時,演算法BFS和BFSM所給出BFS序列就是惟一的。
5、演算法分析
對於具有n個頂點和e條邊的無向圖或有向圖,每個頂點均入隊一次。廣度優先遍歷(BFSTraverse)圖的時間複雜度和DFSTraverse演算法相同。
當圖是連通圖時,BFSTraverse演算法只需呼叫一次BFS或BFSM即可完成遍歷操作,此時BFS和BFSM的時間複雜度分別為O(n+e)和0(n2)。
==========================================================================
圖的深度優先和廣度優先遍歷演算法
#include <stdio.h>
#include <stdlib.h>
#define MaxN 30 /*圖中定點數的最大值*/
typedef struct ArcNode /*鄰接連結串列的表節點*/
{ int adjvvex; /*鄰接頂點的頂點序號*/
double weight; /*邊上的權值*/
struct ArcNode *nextarc; /*下一個鄰接定點*/
}EdgeNode;
typedef struct VNode /*鄰接連結串列的頭節點*/
{ char data; /*定點表示的資料,以一個字元表示*/
struct ArcNode *firstarc; /*指向第一個依附於該定點的邊的指標*/
}AdjList[MaxN];
typedef struct
{ int Vnum; /*圖中定點的數目*/
AdjList Vertices;
}Graph;
int visited[MaxN]={0}; /*呼叫遍歷演算法前所有的定都沒有被訪問過*/
void Dfs(Graph G,int i)
{ EdgeNode *t;
int j;
printf("%d",i); /*訪問序號為i的頂點*/
visited[i]=1; /*序號為i的定點已訪問過*/
t=G.Vertices[i].firstarc; /*取定點i的第一個鄰接定點*/
while (t!=NULL) /*檢查所有與頂點i相鄰接的頂點*/
{ j=t->adjvex; /*頂點j為頂點i的一個鄰接頂點*/
if(visited[j]==0) /*若頂點j未被訪問過*/
Dfs(g,j); /*從頂點j出發進行深度優先搜尋*/
t=t->nextarc; /*取頂點i的下一個鄰接頂點*/
}
}
void Bfs(Graph G) /*廣度優先遍歷圖G*/
{ EdgeNode *t;
int i,j,k;
int visted[G.Vnum]={0}; /*呼叫遍歷演算法前所有的定都沒有被訪問過*/
InitQueue(Q); /*建立一個空佇列*/
for (i=0;i<G.Vnum ;i++ )
{ if (!visit[i])
{ EnQueue(Q,i); /*訪問頂點i*/
printf("%d",j);
visited[i]=1;
while (!Empty(Q))
{ Dequeue(Q,k);
for (t=G.Vertices[k].firstarc ; t ;t=t->nextarc )
{ j=t->adjvex;
if (visted[j]==0)
{ EnQueue(Q,j);
printf("%d",j);
visited[j]=1;
}
}
}
}
}
}
=========================================================================
深度優先搜尋
現在我們用堆疊解決一個有意思的問題,定義一個二維陣列:
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;
top++;
}
struct point pop(void)
{
top--;
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陣列儲存的路線從終點列印到起點。為了幫助理解,我把這個演算法改寫成虛擬碼如下:
將起點標記為已走過並壓棧;
while (棧非空) {
從棧頂彈出一個點p;
if (p這個點是終點)
break;
否則沿右、下、左、上四個方向探索相鄰的點,if (和p相鄰的點有路可走,並且還沒走過)
將相鄰的點標記為已走過並壓棧,它的前趨就是p點;
}
if (p點是終點) {
列印p點的座標;
while (p點有前趨) {
p點=p點的前趨;
列印p點的座標;
}
} else
沒有路線可以到達終點;
我在while迴圈的末尾插了列印語句,每探索一步都打印出當前標記了哪些點,從列印結果可看出這種搜尋演算法的特點:每次取一個相鄰的點走下去,一直走到無路可走了再退回來,取另一個相鄰的點再走下去。這稱為深度優先搜尋(DFS,Depth First Search)。探索迷宮和堆疊變化的過程如下圖所示。
圖 12.2. 深度優先搜尋
圖中各點的編號反映出探索的順序,堆疊中的數字就是圖中點的編號,可見正是因為堆疊後進先出的性質使這個演算法具有了深度優先的特點。如果在探索問題的解時走進了死衚衕,則需要退回來從另一條路繼續探索,這種思想稱為回溯(Backtrack),一個典型的例子是很多程式設計書上都會講的八皇后問題。
最後我們列印終點的座標並通過predecessor資料結構找到它的前趨,這樣順藤摸瓜一直列印到起點。那麼能不能從起點到終點正向列印路線呢?在上一節我們看到,如果是在一個迴圈裡列印陣列,既可以正向列印也可以反向列印,因為陣列這種資料結構是支援隨機訪問的,當然也支援順序訪問,並且既可以是正向的也可以是反向的。但現在predecessor這種資料結構的每個元素只知道它的前趨是誰,而不知道它的後繼(Successor)是誰,所以在迴圈裡只能反向列印。由此可見,有什麼樣的資料結構就決定了可以用什麼樣的演算法。那麼,為什麼不再建一個successor陣列來儲存每個點的後繼呢?雖然每個點的前趨只有一個,後繼卻不止一個,從DFS演算法的過程可以看出,如果每次在儲存前趨的同時也儲存後繼,後繼不一定會指向正確的路線,請讀者想一想為什麼。由此可見,有什麼樣的演算法就決定了可以用什麼樣的資料結構。設計演算法和設計資料結構這兩件工作是緊密聯絡的。
習題
1、修改本節的程式,最後從起點到終點正向列印路線。你能想出幾種辦法?
2、本節程式中predecessor這個資料結構佔用的儲存空間太多了,可以改變它的儲存方式以節省空間,想一想該怎麼做。
3、上一節我們實現了一個基於堆疊的程式,然後用遞迴改寫了它,用函式呼叫的棧幀實現同樣的功能。本節的DSF演算法是基於堆疊的,請把它改寫成遞迴的程式。改寫成遞迴程式是可以避免使用predecessor資料結構的,想想該怎麼做。
轉載宣告: 本文轉自 http://www.yayu.org/book/Linux_c_study_html/ch12s03.html
==============================================================================
佇列與廣度優先搜尋
佇列也是一組元素的集合,也提供兩種基本操作:Enqueue(入隊)將元素新增到隊尾,Dequeue(出隊)從隊頭取出元素並返回。就像排隊買票一樣,先來先服務,先入隊的人也是先出隊的,這種方式稱為FIFO(First In First Out,先進先出),有時候佇列本身也被稱為FIFO。
下面我們用佇列解決迷宮問題。程式如下:
例 12.4. 用廣度優先搜尋解迷宮問題
#include <stdio.h>
#define MAX_ROW 5
#define MAX_COL 5
struct point { int row, col, predecessor; } queue[512];
int head = 0, tail = 0;
void enqueue(struct point p)
{
queue[tail] = p;
tail++;
}
struct point dequeue(void)
{
head++;
return queue[head-1];
}
int is_empty(void)
{
return head == tail;
}
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");
}
void visit(int row, int col)
{
struct point visit_point = { row, col, head-1 };
maze[row][col] = 2;
enqueue(visit_point);
}
int main(void)
{
struct point p = { 0, 0, -1 };
maze[p.row][p.col] = 2;
enqueue(p);
while (!is_empty()) {
p = dequeue();
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);
if (p.row+1 < MAX_ROW /* down */
&& maze[p.row+1][p.col] == 0)
visit(p.row+1, p.col);
if (p.col-1 >= 0 /* left */
&& maze[p.row][p.col-1] == 0)
visit(p.row, p.col-1);
if (p.row-1 >= 0 /* up */
&& maze[p.row-1][p.col] == 0)
visit(p.row-1, p.col);
print_maze();
}
if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) {
printf("(%d, %d)/n", p.row, p.col);
while (p.predecessor != -1) {
p = queue[p.predecessor];
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 2 0 0
2 1 1 1 0
0 0 0 1 0
*********
2 1 0 0 0
2 1 0 1 0
2 2 2 0 0
2 1 1 1 0
2 0 0 1 0
*********
2 1 0 0 0
2 1 2 1 0
2 2 2 2 0
2 1 1 1 0
2 0 0 1 0
*********
2 1 0 0 0
2 1 2 1 0
2 2 2 2 0
2 1 1 1 0
2 2 0 1 0
*********
2 1 0 0 0
2 1 2 1 0
2 2 2 2 2
2 1 1 1 0
2 2 0 1 0
*********
2 1 2 0 0
2 1 2 1 0
2 2 2 2 2
2 1 1 1 0
2 2 0 1 0
*********
2 1 2 0 0
2 1 2 1 0
2 2 2 2 2
2 1 1 1 0
2 2 2 1 0
*********
2 1 2 0 0
2 1 2 1 2
2 2 2 2 2
2 1 1 1 2
2 2 2 1 0
*********
2 1 2 2 0
2 1 2 1 2
2 2 2 2 2
2 1 1 1 2
2 2 2 1 0
*********
2 1 2 2 0
2 1 2 1 2
2 2 2 2 2
2 1 1 1 2
2 2 2 1 0
*********
2 1 2 2 0
2 1 2 1 2
2 2 2 2 2
2 1 1 1 2
2 2 2 1 2
*********
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
*********
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)
(2, 3)
(2, 2)
(2, 1)
(2, 0)
(1, 0)
(0, 0)
其實仍然可以像例 12.3 “用深度優先搜尋解迷宮問題”一樣用predecessor陣列表示每個點的前趨,但是我想換一種更方便的資料結構,直接在每個點的結構體中加一個成員表示前趨:
struct point { int row, col, predecessor; } queue[512];
int head = 0, tail = 0;
變數head、tail就像前兩節用來表示棧頂的top一樣,是queue陣列的索引或者叫指標,分別指向隊頭和隊尾。每個點的predecessor成員也是一個指標,指向它的前趨在queue陣列中的位置。如下圖所示:
圖 12.3. 廣度優先搜尋的佇列資料結構
為了幫助理解,我把這個演算法改寫成虛擬碼如下:
將起點標記為已走過併入隊;
while (佇列非空) {
出隊一個點p;
if (p這個點是終點)
break;
否則沿右、下、左、上四個方向探索相鄰的點,if (和p相鄰的點有路可走,並且還沒走過)
將相鄰的點標記為已走過併入隊,它的前趨就是剛出隊的p點;
}
if (p點是終點) {
列印p點的座標;
while (p點有前趨) {
p點=p點的前趨;
列印p點的座標;
}
} else
沒有路線可以到達終點;
從列印的搜尋過程可以看出,這個演算法的特點是沿各個方向同時展開搜尋,每個可以走通的方向輪流往前走一步,這稱為廣度優先搜尋(BFS,Breadth First Search)。探索迷宮和佇列變化的過程如下圖所示。
圖 12.4. 廣度優先搜尋
廣度優先是一種步步為營的策略,每次都從各個方向探索一步,將前線推進一步,圖中的虛線就表示這個前線,佇列中的元素總是由前線的點組成的,可見正是因為佇列先進先出的性質使這個演算法具有了廣度優先的特點。廣度優先搜尋還有一個特點是可以找到從起點到終點的最短路徑,而深度優先搜尋找到的不一定是最短路徑,比較本節和上一節程式的執行結果可以看出這一點,想一想為什麼。
轉載宣告: 本文轉自 http://www.yayu.org/book/Linux_c_study_html/ch12s04.html
參考推薦:
12個有趣的C語言面試題