廣度優先搜尋演算法詳解
說到廣度優先搜尋,大家可能先想到的是廣度優先遍歷,其實廣度優先搜尋就是利用了廣度優先遍歷的一種搜尋演算法。我個人總結的該演算法包含以下幾個關鍵點,掌握了這幾個點,該演算法也就掌握的很好了。下面也基本上是圍繞這幾個關鍵點展開的。
1.狀態
2.狀態轉移方式
3.有效狀態
4.佇列
5.標記
我們先來看看一點有意思的吧!
說有一天公主被大魔王抓了,關進了一個迷宮裡,需要你這位勇士去營救(當然成功了就自然是升職加薪贏取白富美啦),這個迷宮以二維陣列的形式給出(暫定為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 ]
其中0表示這裡可以走,1表示這裡是堵牆不能走,每次只能往上下左右四個方向走一個單位,迷宮入口為左上角,而公主在最右下角,問你是否能找到一條最短的路徑成功救出公主,如果能輸出其路徑(每一個路徑以座標形式給出,如(0,0)),不能輸出-1。
當看到什麼最短時間啦,最短路徑啦(當然不是圖那種最短路徑),這就適合用廣度優先遍歷。廣度優先遍歷其實是對狀態的一種遍歷,這裡就要搞清楚什麼是狀態。我們先自己想一下,你看到這道題會有什麼樣的想法,從第一個點出發,可以往右、下走,然後如果往右走,又會延伸出往右、下走,我們用一棵樹來表示((1,2,3)表示x=1,y=2,路徑長度也可以叫時間為3)
這裡我們便可以看到一個點加上一個時間t便構成了一個所謂的狀態,而我們廣度優先搜素就是要按層,一層一層地搜尋這些狀態,直到找到要求狀態為止。上面的圖我們還應該修改修改,因為,第二次到達的點所需要的時間肯定比第一次到達所需要的時間多,如上圖的(0,0,3),所以,我們設定一個標記,凡是我們到達過的點,便不進行第二次的搜尋與延伸,此之謂剪枝
狀態轉移呢,是指由一個狀態可以延伸出其他哪些狀態,也就是可以往哪些地方走的問題。
說了這麼多,是時候上點乾貨了,我們看看程式碼
#include<iostream> #include<queue> #include<stack> using namespace std; int migong[5][5];//儲存迷宮圖 bool flag[5][5];//標記改點是否到達過 class Stat { public: int x,y; int t; Stat* father;//指向其父狀態,用於逆向尋找路徑 }; int R[4][2]={ -1,0, 1,0, 0,-1, 0,1 };//用於狀態擴充套件 Stat* BFS()//返回終點狀態 { queue<Stat*> Q;//用於儲存還沒有被擴充套件的(即將擴充套件)狀態 int x=0,y=0,t=0; Stat* start = new Stat();//這裡是我栽過大坑,由於是在函式內部定義的物件,必須要用new來分配記憶體與堆中 start->x=start->y=0; start->t = 0; start->father = NULL;//開始狀態沒有父狀態 Q.push(start); while(Q.empty() == false)//直到搜尋完所有狀態退出迴圈 { Stat* temp = Q.front(); Q.pop(); for(int i=0;i<4;i++)//這裡就是狀態的擴充套件,向上下左右四個方向擴充套件 { x = temp->x+R[i][0]; y = temp->y+R[i][1]; if(x<0||y<0||x>4||y>4)//超出邊界,便直接捨棄該狀態 continue; if(flag[x][y] == true)//到達過該狀態,也直接捨棄 continue; if(migong[x][y] == 1)//沒有路,也直接捨棄 continue; Stat* tempS = new Stat();//建立新狀態 tempS->x = x; tempS->y = y; tempS->t = temp->t+1;//時間加一 tempS->father = temp;//指向父節點 if(x == 4 && y == 4)//如果搜尋到了目標狀態,便返回 { return tempS; } Q.push(tempS);//將新狀態加入佇列中 flag[x][y] = true;//標記該狀態已經到達過 } } return start; } int main() { for(int i=0;i<5;i++)//迴圈輸入迷宮 { for(int j=0;j<5;j++) { cin>>migong[i][j]; flag[i][j] = false; } } Stat* p = BFS(); stack<Stat*> S;//放入棧中,主要是為了讓其反序,不然從目標狀態找其父節點遍歷的話,是反的 while(p != NULL) { S.push(p); p = p->father; } while(S.empty() == false) { Stat* temp = S.top(); S.pop(); cout<<"("<<temp->x<<","<<temp->y<<")"<<endl; } } /* 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 */
相信大家看了程式碼,也就大概瞭解的八九不離十了,但這裡需要強調的是這裡面佇列的作用,主要是利用了佇列的先進先出的特性,當一個狀態向4個方向擴充套件時,依次將其放入佇列中,再由這四個狀態繼續擴充套件,這樣佇列狀態的取出便是按照層次的順序,一層一層地遍歷的,這才是廣度優先搜尋的的關鍵。
我們來看看結果
最後,大家一定要記住我踩過的坑(在函式內定義物件時,如果在以後還要用到的話,一定要用new關鍵字分配內存於堆中)
ps: 這是我第一次寫部落格,諸多問題,還請大家指出,海涵。