1. 程式人生 > 實用技巧 >A Star演算法總結與實現(附Demo)

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是一種演算法,一種工具,在經歷種種反覆的推敲之後,儼然成為了一種思想,一種在未知領域尋找最優解的思想。

傳送門

A*與JPS尋路演算法的實現Demo

專業的各種尋路演算法的Demo

我自己的WebGl Demo

AStarDemo的Github工程

參考與引用

維基百科-A* search algorithm

A星尋路演算法介紹-莫水千流-部落格園

A Star Algorithm總結與實現

A*多人尋路解決方案及優化策略-CSDN