BFS、雙向BFS和A* Table of Contents 1. BFS 2. 雙向BFS 3. A*演算法
光說不練是沒用的,我們從廣為人知的POJ 2243這道題談起:題目大意:給定一個起點和一個終點,按騎士的走法(走日字),從起點到終點的最少移動多少次
設A為尋路起點,B為目標終點。 1 BFS
BFS其實是退化的A*演算法,因為他沒有啟發函數做指引 Memory Time 144K 407MS
簡單的代碼如下:
#include<iostream>#include<queue>using namespace std;char ss[3];char ee[3];typedef struct node{ int x; int y; int steps;}node;int d[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};int visited[8][8];node s;node e;int in(node n){ if(n.x<0||n.y<0||n.x>7||n.y>7) return 0; return 1;}void bfs(){ queue<node>q; memset(visited,0,sizeof(visited)); q.push(s); visited[s.x][s.y]=1; while(!q.empty()) { node st=q.front(); q.pop(); if(st.x==e.x&&st.y==e.y) { printf("To get from %s to %s takes %d knight moves.\n",ss,ee,st.steps); break; } for(int i=0;i<8;++i) { node t; t.x=st.x+d[i][0]; t.y=st.y+d[i][1]; if(in(t)&&visited[t.x][t.y]==0) { visited[t.x][t.y]=1; t.steps=st.steps+1; q.push(t); } } }}int main(int argc, char *argv[]){ while(scanf("%s %s",ss,ee)==2) { s.x=ss[0]-'a'; s.y=ss[1]-'1'; e.x=ee[0]-'a'; e.y=ee[1]-'1'; bfs(); } return 0;} 2 雙向BFS
雙向bfs就是用兩個隊列,一個隊列儲存從起點開始的狀態,另一個儲存從終點開始向前搜尋的狀態,雙向bfs主要是區分每個格子是從起點開始搜尋到的還是從終點開始搜尋到的.每個經過的格子結點儲存到達該格子經過的步數,這樣兩邊要是相交了相加就是結果 Memory Time 144K 141MS
明顯的省時間
#include<iostream>#include<queue>using namespace std;char ss[3];char ee[3];typedef struct node{ int x; int y; int steps;}node;int d[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};int visited[8][8];int color[8][8];//區分當前位置是哪個隊列尋找過了node s;node e;int in(node n){ if(n.x<0||n.y<0||n.x>7||n.y>7) return 0; return 1;}int bfs(){ queue<node>qf; //我發現如果把qf和qb放在外面的話,節省的時間挺驚人的,耗時16MS queue<node>qb; memset(visited,0,sizeof(visited)); memset(color,0,sizeof(color)); qf.push(s); qb.push(e); visited[s.x][s.y]=0; visited[e.x][e.y]=1; color[s.x][s.y]=1;//著色 color[e.x][e.y]=2; while(!qf.empty()||!qb.empty()) { if(!qf.empty()) { node st=qf.front(); qf.pop(); for(int i=0;i<8;++i) { node t; t.x=st.x+d[i][0]; t.y=st.y+d[i][1]; if(in(t)) { if(color[t.x][t.y]==0){ visited[t.x][t.y]=visited[st.x][st.y]+1; color[t.x][t.y]=1; qf.push(t); } else if(color[t.x][t.y]==2){ return visited[st.x][st.y]+visited[t.x][t.y]; } } } } if(!qb.empty()) { node st=qb.front(); qb.pop(); for(int i=0;i<8;++i) { node t; t.x=st.x+d[i][0]; t.y=st.y+d[i][1]; if(in(t)) { if(color[t.x][t.y]==0){ visited[t.x][t.y]=visited[st.x][st.y]+1; color[t.x][t.y]=2; qb.push(t); } else if(color[t.x][t.y]==1){ return visited[st.x][st.y]+visited[t.x][t.y]; } } } } }}int main(int argc, char *argv[]){ // freopen("in.txt","r",stdin); while(scanf("%s %s",ss,ee)==2) { s.x=ss[0]-'a'; s.y=ss[1]-'1'; e.x=ee[0]-'a'; e.y=ee[1]-'1'; s.steps=0; e.steps=1; if(s.x==e.x&&s.y==e.y) printf("To get from %s to %s takes 0 knight moves.\n",ss,ee); else printf("To get from %s to %s takes %d knight moves.\n",ss,ee,bfs()); } return 0;} 3 A*演算法
選擇路徑中經過哪個方格的關鍵是下面這個等式:F = G + H這裡: G = 從起點A,沿著產生的路徑,移動到網格上指定方格的移動耗費。 H = 從網格上那個方格移動到終點B的預估移動耗費。這經常被稱為啟發學習法的,可能會讓你有點迷惑。這樣叫的原因是因為它只是個猜測。我們沒辦法事Crowdsourced Security Testing道路徑的長度,因為路上可能存在各種障礙(牆,水,等等)。
A*演算法步驟為: 把起始格添加到開啟列表。 重複如下的工作: 尋找開啟列表中F值最低的格子。我們稱它為當前格。 把它切換到關閉列表。 對相鄰的格中的每一個。 如果它不可通過或者已經在關閉列表中,略過它。反之如下。 如果它不在開啟列表中,把它添加進去。把當前格作為這一格的父節點。記錄這一格的F,G,和H值。 如果它已經在開啟列表中,用G值為參考檢查新的路徑是否更好。更低的G值意味著更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之後你可能需要重新對開啟列表排序。 停止,當你 把目標格添加進了關閉列表,這時候路徑被找到,或者 沒有找到目標格,開啟列表已經空了。這時候,路徑不存在。 儲存路徑。從目標格開始,沿著每一格的父節點移動直到回到起始格。這就是你的路徑。
可以這樣說,BFS是A*演算法的一個特例。對於一個BFS演算法,從當前節點擴充出來的每一個節點(如果沒有被訪問過的話)都要放進隊列進行進一步擴充。也就是說BFS的估計函數h永遠等於0,沒有一點啟發學習法的資訊,可以認為BFS是“最爛的”A*演算法。
選取最小估價:如果學過資料結構的話,應該可以知道,對於每次都要選取最小估價的節點,應該用到最小優先順序隊列(也叫最小二元堆積)。在C++的STL裡有現成的資料結構priorityqueue,可以直接使用。當然不要忘了重載自訂節點的比較操作符。 Memory Time 154K 47MS
不過上面最佳化的雙向BFS(16MS)
#include<iostream>#include<queue>#include<stdlib.h>using namespace std;char ss[3];char ee[3];typedef struct node{ int x; int y; int steps; int g; int h; int f; friend bool operator < (const node & a,const node &b);}node;inline bool operator < (const node & a,const node &b){ return a.f>b.f;}int d[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};int visited[8][8];node s;node e;int in(node n){ if(n.x<0||n.y<0||n.x>7||n.y>7) return 0; return 1;}int Heuristic(const node &a){ return (abs(a.x-e.x)+abs(a.y-e.y))*10;}//曼哈頓(manhattan)估價函數priority_queue<node> q; //最小優先順序隊列(開啟列表) 這裡有點最佳化策略,因為我發現如果把q //放在Astar函數裡頭的話,代碼跑起來是157MS,放在外面的話是47MS,有顯著的區別int Astar(){ while(!q.empty())q.pop(); memset(visited,0,sizeof(visited)); q.push(s); while(!q.empty()) { node front=q.top(); node t; q.pop(); visited[front.x][front.y]=1; if(front.x==e.x && front.y==e.y) return front.steps; for(int i=0;i<8;i++){ t.x=front.x+d[i][0]; t.y=front.y+d[i][1]; if(in(t) && visited[t.x][t.y]==0){ t.g=23+front.g; t.h=Heuristic(t); t.f=t.g+t.h; t.steps=front.steps+1; q.push(t); } } }}int main(int argc, char *argv[]){ //freopen("in.txt","r",stdin); while(scanf("%s %s",ss,ee)==2) { s.x=ss[0]-'a'; s.y=ss[1]-'1'; e.x=ee[0]-'a'; e.y=ee[1]-'1'; s.steps=0; s.g=0; s.h=Heuristic(s); s.f=s.g+s.h; if(s.x==e.x&&s.y==e.y) printf("To get from %s to %s takes 0 knight moves.\n",ss,ee); else printf("To get from %s to %s takes %d knight moves.\n",ss,ee,Astar()); } return 0;}
本篇文章摘錄了最基本的BFS和雙向BFS的實現以及A*的基本原理,由於原理不是十分難懂又有圖解過程,所以可以一次性掌握原理(雖然文字介紹相當簡要,不過好像也沒有什麼要說的),剩下的動手的問題。如果你有任何建議或者批評和補充,請留言指出,不勝感激,更多參考請移步互連網。
Author: kirchhoff
Created: 2014-11-14 Fri 17:43
Emacs 24.4.1 (Org mode 8.2.10)
Validate