BFS、雙向BFS和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的預估移動耗費。
這常常被稱為啟示式的,可能會讓你有點迷惑。
這樣叫的原因是由於它僅僅是個推測。我們沒辦法事先知道路徑的長度,由於路上可能存在各種障礙(牆,水。等等)。
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*的基本原理。因為原理不是十分難懂又有圖解過程,所以能夠一次性掌握原理(盡管文字介紹相當簡要,只是好像也沒有什么要說的)。剩下的動手的問題。
假設你有不論什么建議或者批評和補充,請留言指出,不勝感激,很多其它參考請移步互聯網。
版權聲明:本文博主原創文章,博客,未經同意不得轉載。