A Star演算法總結與實現(附Demo)
關於A Star Algorithm
A star演算法最早可追溯到1968年,在IEEE Transactions on Systems Science and Cybernetics中的一篇A Formal Basis for the Heuristic Determination of Minimum Cost Paths,是把啟發式方法(heuristic approaches)如BFS,和常規方法如Dijsktra演算法結合在一起的演算法。有點不同的是,類似BFS的啟發式方法經常給出一個近似解而不是保證最佳解。然而,儘管A star基於無法保證最佳解的啟發式方法,A star卻能保證找到一條最短路徑。
公式表示為:f(n)=g(n)+h(n)
f(n)是節點n從初始點到目標點的估價函式
g(n)是在狀態空間中從初始節點到n節點的實際代價
h(n)是從n到目標節點最佳路徑的估計代價
觀察A*尋路演算法的執行軌跡
假設起點為A(淺藍色的個字) 終點為B(深藍色的格子)
紅色代表該格子為障礙物
地圖為20x20的格子
顯示FGH值的格子代表經過A*演算法搜尋並生成路徑的格子
有透明度變化的格子代表該格子有被搜尋。
綠色格子代表的是搜尋完成後A*得到的最優的路徑
A直接抵達B的情況下
A越過直線障礙到達B
A越過U型障礙到達B
B為障礙物所包圍著,A到達不了B的情況下
總結與思考
由4組圖可以得到
1.A*的消耗是一個及其不穩定的過程,消耗的最小值不低於直線路徑上的消耗,消耗的最大值不高於遍歷整張地圖的消耗。
2.A*的消耗主要在搜尋的搜尋格子,以及對其FGH的操作上。
3.由1,2可以得出,在對執行速率和效率有要求的場景下,A*可能不是一個比較好選擇。
演算法步驟
橫向縱向的格子的單位消耗為10,對角單位消耗為14。
定義一個OpenList,用於儲存和搜尋當前最小值的格子。
定義一個CloseList,用於標記已經處理過的格子,以防止重複搜尋。
開始搜尋尋路
1.將起點加入OpenList
2.從OpenList中彈出F值最小的點作為當前點
3.獲取當前點九空格(除去自己)內所有的非障礙且不在CloseList內的鄰居點
4.遍歷上一步驟得到的鄰居點的集合,每個鄰居點執行以下邏輯
如果鄰居點在OpenList中
計算當前值的G與該鄰居點的G值
如果G值比該鄰居點的G值小
將當前點設定為該鄰居點的父節點
更新該鄰居點的GF值
若不在
計算並設定當前點與該鄰居點的G值
計算並設定當前點與該鄰居點的H值
計算並設定該鄰居點的F值
將當前點設定為該鄰居點的父節點
5.判斷終點是否在OpenList中,如果已在OpenList中,則返回該點,其父節點連起來的路徑就是A*搜尋的路徑。如果不在,則重複執行2,3,4,5。直到找到終點,或者OpenList中節點數量為0。
Tip:判定結束的有兩種
第一種是以OpenList中有終點節點或者OpenList中沒有節點
第二種是CLoseList中有終點節點或者......
第一種要比第二種運算次數要少許多,但在最短路徑的的處理上,第二種要比第一種要精準,是相對精準。
圖解演算法
(7,10)為起點 ,(11,10)為終點,(9,11) (9,10)(9,9)為障礙點。
1.當前點為(7,10)
2.當前點為(8,9)
2.當前點為(8,11)
當前點為(6,10)
這裡是最容易忽視的地方,因為A*的啟發搜尋的實現就是靠搜尋F值最小的節點來實現,所以是會出現這種背離目標的搜尋。
當前點為(7,9)
當前點為(7,11)
當前點為(9,8)
當前點為(10,9)
當OpenList中出現終點節點時,則結束此次搜尋
如果有想看更復雜的條件下的搜尋軌跡,線上的AStarDemo 或者clone github工程
總結與思考
A的消耗有很大的不確定性。消耗跟地圖的複雜程度成正比,跟相對距離的長短成正比。
有一個極端的情況,當終點位置為障礙點包圍時,即A Star找不到終點座標,A會遍歷該地圖此障礙區以外的所有區域。
關鍵邏輯的程式碼實現
1.A*尋路演算法的主邏輯
Point start = ...;
Point end = ...;
bool isIgnoreCorner = ...;
OpenList.Add(start);
while (OpenList.Count != 0)
{
stepSearch(start, end, isIgnoreCorner);
if (OpenList.Get(end) != null)
return OpenList.Get(end);
}
return OpenList.Get(end);
2.單次搜尋所執行的邏輯
//找出F值最小的點
var tempPoint = OpenList.PopMinPoint();
//OpenList.RemoveAt(0);
CloseList.Add(tempPoint);
var alivePoints = GetGridAlivePoint(tempPoint, isIgnoreCorner);
for (int i = 0; i < alivePoints.Count; i++)
{
Point p = alivePoints[i];
if (OpenList.Exists(p))
{
//計算G值, 如果比原來的大, 就什麼都不做, 否則設定它的父節點為當前點,並更新G和F
FoundPoint(tempPoint, p);
}
else
{
//如果它們不在開始列表裡, 就加入, 並設定父節點,並計算GHF
NotFoundPoint(tempPoint, end, p);
}
}
3.當鄰居點在OpenList點中時的處理邏輯
var G = CalcG(tempStart, point);
if (G < point.G)
{
point.ParentPoint = tempStart;
//因為每次取值,都是使用F值,所以我覺的可以不更新G值
//point.G = G;
point.F = point.H + G;
}
4.當鄰居點不在OpenList點中時的處理邏輯
point.ParentPoint = tempStart;
point.G = CalcG(tempStart, point);
point.H = CalcH(end, point);
point.CalcF();
OpenList.Add(point);
5.最基礎的邏輯也是最重要的邏輯之一,計算G值
計算G值 只適用於相鄰的兩個點
int G = (Math.Abs(point.X - start.X) + Math.Abs(point.Y - start.Y)) == 2 ? 14:10;
int parentG = point.ParentPoint != null ? point.ParentPoint.G : 0;
return G + parentG;
5.最基礎的邏輯也是最重要的邏輯之一,計算H值
同G值,這裡只計算直線上的消耗,不處理對角。
int step = Math.Abs(point.X - end.X) + Math.Abs(point.Y - end.Y);
return step * 10;
應用與思考
1.A* 在遊戲中多有應用,怪物AI,計算玩家行走的路徑,一些輔助工具比如遊戲機器人玩家的策略方案等應用。但因為其消耗的極其不穩定,所以不會作為首選,在遊戲中如果大量的應用這種邏輯,JPS(Jump Search Point),或者JPS+(JPS的優化版本)
2.A*在AR和自動駕駛領域也有應用。比如有些AR的應用是基於SLAM演算法進行場景實時建模,然後在生成的模型當中,搜尋一條有效的路徑。A Star在這種場景中有很強的應用空間。
3.A Star的消耗主要是不斷的搜尋生成新的節點,不斷的遍歷計算。其優化思路一般也是圍繞這兩個點,減少搜尋次數,優化遍歷方案。我個人覺得JPS(Jump Point Search )就是把A Star優化做到一定程度的結果。
4.第一篇關於A Star文章是在1968年,第一篇關於JPS的文章是在2011年。在這段時間A Star處於什麼樣的一個地位,在這期間A Star又經歷了什麼樣的演變,又演變出多少種在其基礎之上優化的演算法。在我看來剛出世時的A Star是一種演算法,一種工具,在經歷種種反覆的推敲之後,儼然成為了一種思想,一種在未知領域尋找最優解的思想。