启发式搜索A*算法
A*?尋路算法
?(2011-02-15 10:53:11) 轉載▼標簽:? 游戲 | 分類:?算法 |
概述
雖然掌握了 A* 算法的人認為它容易,但是對于初學者來說, A* 算法還是很復雜的。
搜索區域(The Search Area)
我們假設某人要從 A 點移動到 B 點,但是這兩點之間被一堵墻隔開。如圖 1 ,綠色是 A ,紅色是 B ,中間藍色是墻。
?
?
圖 1
你應該注意到了,我們把要搜尋的區域劃分成了正方形的格子。這是尋路的第一步,簡化搜索區域,就像我們這里做的一樣。這個特殊的方法把我們的搜索區域簡化為了 2 維數組。數組的每一項代表一個格子,它的狀態就是可走 (walkalbe) 和不可走 (unwalkable) 。通過計算出從 A 到 B 需要走過哪些方格,就找到了路徑。一旦路徑找到了,人物便從一個方格的中心移動到另一個方格的中心,直至到達目的地。
方格的中心點我們成為“節點 (nodes) ”。如果你讀過其他關于 A* 尋路算法的文章,你會發現人們常常都在討論節點。為什么不直接描述為方格呢?因為我們有可能把搜索區域劃為為其他多變形而不是正方形,例如可以是六邊形,矩形,甚至可以是任意多變形。而節點可以放在任意多邊形里面,可以放在多變形的中心,也可以放在多邊形的邊上。我們使用這個系統,因為它最簡單。
開始搜索(Starting the Search)
一旦我們把搜尋區域簡化為一組可以量化的節點后,就像上面做的一樣,我們下一步要做的便是查找最短路徑。在 A* 中,我們從起點開始,檢查其相鄰的方格,然后向四周擴展,直至找到目標。
我們這樣開始我們的尋路旅途:
1.???????從起點 A 開始,并把它就加入到一個由方格組成的 open list( 開放列表 ) 中。這個 open list 有點像是一個購物單。當然現在 open list 里只有一項,它就是起點 A ,后面會慢慢加入更多的項。 Open list 里的格子是路徑可能會是沿途經過的,也有可能不經過。基本上 open list 是一個待檢查的方格列表。
2.???????查看與起點 A 相鄰的方格 ( 忽略其中墻壁所占領的方格,河流所占領的方格及其他非法地形占領的方格 ) ,把其中可走的 (walkable) 或可到達的 (reachable) 方格也加入到 open list 中。把起點 A 設置為這些方格的父親 (parent node 或 parent square) 。當我們在追蹤路徑時,這些父節點的內容是很重要的。稍后解釋。
3.???????把 A 從 open list 中移除,加入到 close list( 封閉列表 ) 中, close list 中的每個方格都是現在不需要再關注的。
如下圖所示,深綠色的方格為起點,它的外框是亮藍色,表示該方格被加入到了 close list 。與它相鄰的黑色方格是需要被檢查的,他們的外框是亮綠色。每個黑方格都有一個灰色的指針指向他們的父節點,這里是起點 A 。
?
圖 2 。
下一步,我們需要從 open list 中選一個與起點 A 相鄰的方格,按下面描述的一樣或多或少的重復前面的步驟。但是到底選擇哪個方格好呢?具有最小 F 值的那個。
?
路徑排序(Path Sorting)
計算出組成路徑的方格的關鍵是下面這個等式:
F = G + H
這里,
G = 從起點 A 移動到指定方格的移動代價,沿著到達該方格而生成的路徑。
H = 從指定的方格移動到終點 B 的估算成本。這個通常被稱為試探法,有點讓人混淆。為什么這么叫呢,因為這是個猜測。直到我們找到了路徑我們才會知道真正的距離,因為途中有各種各樣的東西 ( 比如墻壁,水等 ) 。本教程將教你一種計算 H 的方法,你也可以在網上找到其他方法。
我們的路徑是這么產生的:反復遍歷 open list ,選擇 F 值最小的方格。這個過程稍后詳細描述。我們還是先看看怎么去計算上面的等式。
如上所述, G 是從起點A移動到指定方格的移動代價。在本例中,橫向和縱向的移動代價為 10 ,對角線的移動代價為 14 。之所以使用這些數據,是因為實際的對角移動距離是 2 的平方根,或者是近似的 1.414 倍的橫向或縱向移動代價。使用 10 和 14 就是為了簡單起見。比例是對的,我們避免了開放和小數的計算。這并不是我們沒有這個能力或是不喜歡數學。使用這些數字也可以使計算機更快。稍后你便會發現,如果不使用這些技巧,尋路算法將很慢。
?
既然我們是沿著到達指定方格的路徑來計算 G 值,那么計算出該方格的 G 值的方法就是找出其父親的 G 值,然后按父親是直線方向還是斜線方向加上 10 或 14 。隨著我們離開起點而得到更多的方格,這個方法會變得更加明朗。
?
有很多方法可以估算 H 值。這里我們使用 Manhattan 方法,計算從當前方格橫向或縱向移動到達目標所經過的方格數,忽略對角移動,然后把總數乘以 10 。之所以叫做 Manhattan 方法,是因為這很像統計從一個地點到另一個地點所穿過的街區數,而你不能斜向穿過街區。重要的是,計算 H 是,要忽略路徑中的障礙物。這是對剩余距離的估算值,而不是實際值,因此才稱為試探法。
?
把 G 和 H 相加便得到 F 。我們第一步的結果如下圖所示。每個方格都標上了 F , G , H 的值,就像起點右邊的方格那樣,左上角是 F ,左下角是 G ,右下角是 H 。
?
圖 3
好,現在讓我們看看其中的一些方格。在標有字母的方格, G = 10 。這是因為水平方向從起點到那里只有一個方格的距離。與起點直接相鄰的上方,下方,左方的方格的 G 值都是 10 ,對角線的方格 G 值都是 14 。
?
H 值通過估算起點于終點 ( 紅色方格 ) 的 Manhattan 距離得到,僅作橫向和縱向移動,并且忽略沿途的墻壁。使用這種方式,起點右邊的方格到終點有 3 個方格的距離,因此 H = 30 。這個方格上方的方格到終點有 4 個方格的距離 ( 注意只計算橫向和縱向距離 ) ,因此 H = 40 。對于其他的方格,你可以用同樣的方法知道 H 值是如何得來的。
?
每個方格的 F 值,再說一次,直接把 G 值和 H 值相加就可以了。
?
繼續搜索(Continuing the Search)
為了繼續搜索,我們從 open list 中選擇 F 值最小的 ( 方格 ) 節點,然后對所選擇的方格作如下操作:
4.???????把它從 open list 里取出,放到 close list 中。
5.???????檢查所有與它相鄰的方格,忽略其中在 close list 中或是不可走 (unwalkable) 的方格 ( 比如墻,水,或是其他非法地形 ) ,如果方格不在 open lsit 中,則把它們加入到 open list 中。
把我們選定的方格設置為這些新加入的方格的父親。
6.???????如果某個相鄰的方格已經在 open list 中,則檢查這條路徑是否更優,也就是說經由當前方格 ( 我們選中的方格 ) 到達那個方格是否具有更小的 G 值。如果沒有,不做任何操作。
相反,如果 G 值更小,則把那個方格的父親設為當前方格 ( 我們選中的方格 ) ,然后重新計算那個方格的 F 值和 G 值。如果你還是很混淆,請參考下圖。
?
圖 4
Ok ,讓我們看看它是怎么工作的。在我們最初的 9 個方格中,還有 8 個在 open list 中,起點被放入了 close list 中。在這些方格中,起點右邊的格子的 F 值 40 最小,因此我們選擇這個方格作為下一個要處理的方格。它的外框用藍線打亮。
?
首先,我們把它從 open list 移到 close list 中 ( 這就是為什么用藍線打亮的原因了 ) 。然后我們檢查與它相鄰的方格。它右邊的方格是墻壁,我們忽略。它左邊的方格是起點,在 close list 中,我們也忽略。其他 4 個相鄰的方格均在 open list 中,我們需要檢查經由這個方格到達那里的路徑是否更好,使用 G 值來判定。讓我們看看上面的方格。它現在的 G 值為 14 。如果我們經由當前方格到達那里, G 值將會為 20( 其中 10 為到達當前方格的 G 值,此外還要加上從當前方格縱向移動到上面方格的 G 值 10) 。顯然 20 比 14 大,因此這不是最優的路徑。如果你看圖你就會明白。直接從起點沿對角線移動到那個方格比先橫向移動再縱向移動要好。
?
當把 4 個已經在 open list 中的相鄰方格都檢查后,沒有發現經由當前方格的更好路徑,因此我們不做任何改變。現在我們已經檢查了當前方格的所有相鄰的方格,并也對他們作了處理,是時候選擇下一個待處理的方格了。
?
因此再次遍歷我們的 open list ,現在它只有 7 個方格了,我們需要選擇 F 值最小的那個。有趣的是,這次有兩個方格的 F 值都 54 ,選哪個呢?沒什么關系。從速度上考慮,選擇最后加入 open list 的方格更快。這導致了在尋路過程中,當靠近目標時,優先使用新找到的方格的偏好。但是這并不重要。 ( 對相同數據的不同對待,導致兩中版本的 A* 找到等長的不同路徑 ) 。
?
我們選擇起點右下方的方格,如下圖所示。
?
圖 5
?
這次,當我們檢查相鄰的方格時,我們發現它右邊的方格是墻,忽略之。上面的也一樣。
我們把墻下面的一格也忽略掉。為什么?因為如果不穿越墻角的話,你不能直接從當前方格移動到那個方格。你需要先往下走,然后再移動到那個方格,這樣來繞過墻角。 ( 注意:穿越墻角的規則是可選的,依賴于你的節點是怎么放置的 )
?
這樣還剩下 5 個相鄰的方格。當前方格下面的 2 個方格還沒有加入 open list ,所以把它們加入,同時把當前方格設為他們的父親。在剩下的 3 個方格中,有 2 個已經在 close list 中 ( 一個是起點,一個是當前方格上面的方格,外框被加亮的 ) ,我們忽略它們。最后一個方格,也就是當前方格左邊的方格,我們檢查經由當前方格到達那里是否具有更小的 G 值。沒有。因此我們準備從 open list 中選擇下一個待處理的方格。
?
不斷重復這個過程,直到把終點也加入到了 open list 中,此時如下圖所示。
?
圖 6
?
注意,在起點下面 2 格的方格的父親已經與前面不同了。之前它的 G 值是 28 并且指向它右上方的方格。現在它的 G 值為 20 ,并且指向它正上方的方格。這在尋路過程中的某處發生,使用新路徑時 G 值經過檢查并且變得更低,因此父節點被重新設置, G 和 F 值被重新計算。盡管這一變化在本例中并不重要,但是在很多場合中,這種變化會導致尋路結果的巨大變化。
?
那么我們怎么樣去確定實際路徑呢?很簡單,從終點開始,按著箭頭向父節點移動,這樣你就被帶回到了起點,這就是你的路徑。如下圖所示。從起點 A 移動到終點 B 就是簡單從路徑上的一個方格的中心移動到另一個方格的中心,直至目標。就是這么簡單!
?
圖 7
?
A*算法總結(Summary of the A* Method)
Ok ,現在你已經看完了整個的介紹,現在我們把所有步驟放在一起:
1.?????????把起點加入 open list 。
2.?????????重復如下過程:
a.?????????遍歷 open list ,查找 F 值最小的節點,把它作為當前要處理的節點。
b.?????????把這個節點移到 close list 。
c.?????????對當前方格的 8 個相鄰方格的每一個方格?
◆?????如果它是不可抵達的或者它在 close list 中,忽略它。否則,做如下操作。
◆?????如果它不在 open list 中,把它加入 open list ,并且把當前方格設置為它的父親,記錄該方格的 F , G 和 H 值。
◆?????如果它已經在 open list 中,檢查這條路徑 ( 即經由當前方格到達它那里 ) 是否更好,用 G 值作參考。更小的 G 值表示這是更好的路徑。如果是這樣,把它的父親設置為當前方格,并重新計算它的 G 和 F 值。如果你的 open list 是按 F 值排序的話,改變后你可能需要重新排序。
d.?????????停止,當你
◆?????把終點加入到了 open list 中,此時路徑已經找到了,或者
◆?????查找終點失敗,并且 open list 是空的,此時沒有路徑。
3.?????????保存路徑。從終點開始,每個方格沿著父節點移動直至起點,這就是你的路徑。
?
啟發式搜索A*算法
?(2011-03-07 11:14:32) 轉載▼標簽:? 游戲 | 分類:?算法 |
據 Drew 所知最短路經算法現在重要的應用有計算機網絡路由算法,機器人探路,交通路線導航,人工智能,游戲設計等等。美國火星探測器核心的尋路算法就是采用的D*(D Star)算法。
????最短路經計算分靜態最短路計算和動態最短路計算。
????靜態路徑最短路徑算法是外界環境不變,計算最短路徑。主要有Dijkstra算法,A*(A Star)算法。?
????動態路徑最短路是外界環境不斷發生變化,即不能計算預測的情況下計算最短路。如在游戲中敵人或障礙物不斷移動的情況下。典型的有D*算法。
這是Drew程序實現的10000個節點的隨機路網三條互不相交最短路
真實路網計算K條路徑示例:節點5696到節點3006,三條最快速路,可以看出路徑基本上走環線或主干路。黑線為第一條,蘭線為第二條,紅線為第三條。約束條件系數為1.2。共享部分路段。顯示計算部分完全由Drew自己開發的程序完成。
??參見?K條路算法測試程序
Dijkstra算法求最短路徑:
Dijkstra算法是典型最短路算法,用于計算一個節點到其他所有節點的最短路徑。主要特點是以起始點為中心向外層層擴展,直到擴展到終點為止。Dijkstra算法能得出最短路徑的最優解,但由于它遍歷計算的節點很多,所以效率低。
Dijkstra算法是很有代表性的最短路算法,在很多專業課程中都作為基本內容有詳細的介紹,如數據結構,圖論,運籌學等等。
Dijkstra一般的表述通常有兩種方式,一種用永久和臨時標號方式,一種是用OPEN, CLOSE表方式,Drew為了和下面要介紹的 A* 算法和 D* 算法表述一致,這里均采用OPEN,CLOSE表的方式。
大概過程:
創建兩個表,OPEN, CLOSE。
OPEN表保存所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點。
1. 訪問路網中里起始點最近且沒有被檢查過的點,把這個點放入OPEN組中等待檢查。
2. 從OPEN表中找出距起始點最近的點,找出這個點的所有子節點,把這個點放到CLOSE表中。
3. 遍歷考察這個點的子節點。求出這些子節點距起始點的距離值,放子節點到OPEN表中。
4. 重復2,3,步。直到OPEN表為空,或找到目標點。
這是在drew 程序中4000個節點的隨機路網上Dijkstra算法搜索最短路的演示,黑色圓圈表示經過遍歷計算過的點由圖中可以看到Dijkstra算法從起始點開始向周圍層層計算擴展,在計算大量節點后,到達目標點。所以速度慢效率低。
提高Dijkstra搜索速度的方法很多,據Drew所知,常用的有數據結構采用Binary heap的方法,和用Dijkstra從起始點和終點同時搜索的方法。
推薦網頁:http://www.cs.ecnu.edu.cn/assist/js04/ZJS045/ZJS04505/zjs045050a.htm
簡明扼要介紹Dijkstra算法,有圖解顯示和源碼下載。
A*(A Star)算法:啟發式(heuristic)算法
A*(A-Star)算法是一種靜態路網中求解最短路最有效的方法。
公式表示為:????????f(n)=g(n)+h(n),?
其中f(n) 是節點n從初始點到目標點的估價函數,
g(n) 是在狀態空間中從初始節點到n節點的實際代價,
h(n)是從n到目標節點最佳路徑的估計代價。
保證找到最短路徑(最優解的)條件,關鍵在于估價函數h(n)的選取:
估價值h(n)<= n到目標節點的距離實際值,這種情況下,搜索的點數多,搜索范圍大,效率低。但能得到最優解。
如果 估價值>實際值, 搜索的點數少,搜索范圍小,效率高,但不能保證得到最優解。
估價值與實際值越接近,估價函數取得就越好。
例如對于幾何路網來說,可以取兩節點間歐幾理德距離(直線距離)做為估價值,即f=g(n)+sqrt((dx-nx)*(dx-nx)+(dy-ny)*(dy-ny));這樣估價函數f在g值一定的情況下,會或多或少的受估價值h的制約,節點距目標點近,h值小,f值相對就小,能保證最短路的搜索向終點的方向進行。明顯優于Dijstra算法的毫無無方向的向四周搜索。
conditions of heuristic
Optimistic (must be less than or equal to the real cost)
As close to the real cost as possible
主要搜索過程:
創建兩個表,OPEN表保存所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點。
遍歷當前節點的各個節點,將n節點放入CLOSE中,取n節點的子節點X,->算X的估價值->
While(OPEN!=NULL)
{
從OPEN表中取估價值f最小的節點n;
if(n節點==目標節點) break;
else
{
if(X in OPEN) 比較兩個X的估價值f //注意是同一個節點的兩個不同路徑的估價值
if( X的估價值小于OPEN表的估價值 )
更新OPEN表中的估價值; //取最小路徑的估價值
if(X in CLOSE) 比較兩個X的估價值 //注意是同一個節點的兩個不同路徑的估價值
if( X的估價值小于CLOSE表的估價值 )
更新CLOSE表中的估價值; 把X節點放入OPEN //取最小路徑的估價值
if(X not in both)
求X的估價值;
并將X插入OPEN表中; //還沒有排序
}
將n節點插入CLOSE表中;
按照估價值將OPEN表中的節點排序; //實際上是比較OPEN表內節點f的大小,從最小路徑的節點向下進行。
}
上圖是和上面Dijkstra算法使用同一個路網,相同的起點終點,用A*算法的情況,計算的點數從起始點逐漸向目標點方向擴展,計算的節點數量明顯比Dijkstra少得多,效率很高,且能得到最優解。
A*算法和Dijistra算法的區別在于有無估價值,Dijistra算法相當于A*算法中估價值為0的情況。
推薦文章鏈接:
Amit?斯坦福大學一個博士的游戲網站,上面有關于A*算法介紹和不少有價值的鏈接????http://theory.stanford.edu/~amitp/GameProgramming/
Sunway寫的兩篇很好的介紹啟發式和A*算法的中文文章并有A*源碼下載:
初識A*算法?http://creativesoft.home.shangdu.net/AStart1.htm
深入A*算法?http://creativesoft.home.shangdu.net/AStart2.htm
需要注意的是Sunway上面文章“深入A*算法”中引用了一個A*的游戲程序進行講解,并有這個源碼的下載,不過它有一個不小的Bug, 就是新的子節點放入OPEN表中進行了排序,而當子節點在Open表和Closed表中時,重新計算估價值后,沒有重新的對Open表中的節點排序,這個問題會導致計算有時得不到最優解,另外在路網權重懸殊很大時,搜索范圍不但超過Dijkstra,甚至搜索全部路網, 使效率大大降低。?
Drew 對這個問題進行了如下修正,當子節點在Open表和Closed表中時,重新計算估價值后,刪除OPEN表中的老的節點,將有新估價值的節點插入OPEN表中,重新排序,經測試效果良好,修改的代碼如下,紅色部分為Drew添加的代碼.添加進程序的相應部分即可。
在函數GenerateSucc()中?
...................................
g=BestNode->g+1;
TileNumS=TileNum((int)x,(int)y);
if ((Old=CheckOPEN(TileNumS)) != NULL)?
{?
for(c=0;c<8;c++)
if(BestNode->Child[c] == NULL)
break;
BestNode->Child[c]=Old;
if (g < Old->g)?
{
Old->Parent=BestNode;
Old->g=g;
Old->f=g+Old->h;
//Drew 在該處添加如下紅色代碼?
//Implement by Drew?
NODE *q,*p=OPEN->NextNode, *temp=OPEN->NextNode;
while(p!=NULL && p->NodeNum != Old->NodeNum)
{
????q=p;
????p=p->NextNode;
}
if(p->NodeNum == Old->NodeNum)
{
???if(p==OPEN->NextNode)
??{
?????temp = temp->NextNode;
?????OPEN ->NextNode = temp;
??}
??else
??q->NextNode = p->NextNode;
?}
Insert(Old); // Insert Successor on OPEN list wrt f?
}?
......................................................?
另一種A*(A Star)算法:
這種算法可以不直接用估價值,直接用Dijkstra算法程序實現A*算法,Drew對它進行了測試,達到和A*完全一樣的計算效果,且非常簡單。
以鄰接矩陣為例,更改原來鄰接矩陣i行j列元素Dij為 Dij+Djq-Diq; 起始點到目標點的方向i->j, 終點q. Dij為(i到j路段的權重或距離)
其中:Djq,Diq的作用相當于估價值 Djq=(j到q的直線距離);Diq=(i到q的直線距離)
原理:i 到q方向符合Dij+Djq > Diq ,取Dij+Djq-Diq 小,如果是相反方向Dij+Djq-Diq會很大。因此達到向目標方向尋路的作用。
動態路網,最短路徑算法 D*
A* 在靜態路網中非常有效(very efficient for static worlds),但不適于在動態路網,環境如權重等不斷變化的動態環境下。?
D*是動態A*(D-Star,Dynamic A Star)卡內及梅隆機器人中心的Stentz在1994和1995年兩篇文章提出,主要用于機器人探路。是火星探測器采用的尋路算法。
Optimal and Efficient Path Planning for Partially-Known Environments
The Focussed D* Algorithm for Real-Time Replanning
主要方法(這些完全是Drew在讀了上述資料和編制程序中的個人理解,不能保證完全正確,僅供參考):
1.先用Dijstra算法從目標節點G向起始節點搜索。儲存路網中目標點到各個節點的最短路和該位置到目標點的實際值h,k(k為所有變化h之中最小的值,當前為k=h。每個節點包含上一節點到目標點的最短路信息1(2),2(5),5(4),4(7)。則1到4的最短路為1-2-5-4。
原OPEN和CLOSE中節點信息保存。
2.機器人沿最短路開始移動,在移動的下一節點沒有變化時,無需計算,利用上一步Dijstra計算出的最短路信息從出發點向后追述即可,當在Y點探測到下一節點X狀態發生改變,如堵塞。機器人首先調整自己在當前位置Y到目標點G的實際值h(Y),h(Y)=X到Y的新權值c(X,Y)+X的原實際值h(X).X為下一節點(到目標點方向Y->X->G),Y是當前點。k值取h值變化前后的最小。
3.用A*或其它算法計算,這里假設用A*算法,遍歷Y的子節點,點放入CLOSE,調整Y的子節點a的h值,h(a)=h(Y)+Y到子節點a的權重C(Y,a),比較a點是否存在于OPEN和CLOSE中,方法如下:
while()
{
從OPEN表中取k值最小的節點Y;
遍歷Y的子節點a,計算a的h值 h(a)=h(Y)+Y到子節點a的權重C(Y,a)
{
????if(a in OPEN)?????比較兩個a的h值?
????if( a的h值小于OPEN表a的h值 )
????{
更新OPEN表中a的h值;k值取最小的h值
??????????有未受影響的最短路經存在
??????????break;?
????}
????if(a in CLOSE) 比較兩個a的h值 //注意是同一個節點的兩個不同路徑的估價值
????if( a的h值小于CLOSE表的h值 )
????{
更新CLOSE表中a的h值; k值取最小的h值;將a節點放入OPEN表
??????????有未受影響的最短路經存在
??????????break;
????}
????if(a not in both)
????????將a插入OPEN表中; //還沒有排序
}
放Y到CLOSE表;
OPEN表比較k值大小進行排序;
}
機器人利用第一步Dijstra計算出的最短路信息從a點到目標點的最短路經進行。
D*算法在動態環境中尋路非常有效,向目標點移動中,只檢查最短路徑上下一節點或臨近節點的變化情況,如機器人尋路等情況。對于距離遠的最短路徑上發生的變化,則感覺不太適用。?
上圖是Drew在4000個節點的隨機路網上做的分析演示,細黑線為第一次計算出的最短路,紅點部分為路徑上發生變化的堵塞點,當機器人位于982點時,檢測到前面發生路段堵塞,在該點重新根據新的信息計算路徑,可以看到圓圈點為重新計算遍歷過的點,僅僅計算了很少得點就找到了最短路,說明計算非常有效,迅速。綠線為計算出的繞開堵塞部分的新的最短路徑。
總結
- 上一篇: Git客户端(Windows系统)的使用
- 下一篇: VSTO学习笔记(二)Excel对象模型