A*尋路算法

寫在前面的話


無心中在cocoaChina的首頁看到了一篇介紹A*算法用swift實現的文章,對A*尋路算法產生了興趣。在百度谷歌了不少文章後,終於A*算法的流程,同時讓我發現了兩篇很是好的英文文章:html

A* Pathfinding for Beginnersjava

Introduction to A*node

第一篇文章是很是好的A*算法入門文章,通讀一遍就基本能夠用代碼實現了;第二篇文章能夠說給我帶來了震撼,原來算法能夠這樣講,推薦你們都看一下。算法

看完第二篇文章就產生了要學習做者的方式講一下A*算法的衝動,同時也當是練練手,很久沒寫javaScript了。swift

講解方式也按照<Introduction to A*>一文中的順序,從最簡單的廣度優先算法(Breadth-First-Search)、大名鼎鼎的Dijkstra算法到Greed-Best-First-Search算法,最後是A*算法。app

(關於A*算法,網上資料不少,A*算法的變種也不少,有興趣的朋友能夠自行搜索,
本文僅對四種常見尋路算法進行簡單介紹,如有不合理或錯誤之處,請諒解並在回覆中指出)性能

廣度優先算法


廣度優先算法是最簡單的尋路算法,算法執行的結果是得到從地圖上任意一點S到其餘全部可達點的最短路徑,這裏只考慮上下左右四方向行走的狀況,算法流程很是容易理解:學習

  1. 設定搜索起點S,放入openList中;
  2. 判斷openList是否爲空,若爲空,搜索結束;若不爲空,拿出openList中的第一個節點G;
  3. 遍歷G的上下左右四個相鄰節點N1-N4,對每一個節點N,若是N不在openList或closeList中,那麼令N的父節點爲G,將N放入openList中;若是N已經在openList或closeList中,跳過不處理;
  4. 將G放入closeList中,重複步驟2.

演示gif:測試

alt text

演示程序優化

藍色方塊是不可經過的,S爲掃描的起始點,一層層向外擴展,最終全部可到達的節點都被掃描,這一過程有時被稱爲「flood fill」。對於每個被掃描的節點,爲其添加一個指向父節點的方向箭頭,而後你會發現,從地圖上任意一點開始,只要沿着箭頭的方向移動,總能走到起始點S,並且走過的路徑必然是最短路徑之一。

看到廣度優先算法,最早想到的應用場景就是塔防,敵人老是從固定的一個或幾個出生點出現,向着固定的一個或幾個目標移動,咱們徹底能夠在每一關開始前以出生點爲起始點遍歷整個地圖,這樣本關中怪物的移動路線就能夠肯定了。

下面讓咱們考慮如下場景,地圖中存在森林、山嶺和平原,角色在這些地形上移動時,移動力消耗是不一樣的,好比《文明》中。這就要求咱們把每個區塊的消耗考慮在內,這時,Dijkstra算法就能夠發揮做用了。

Dijkstra算法


在地圖內的每一個區塊移動消耗不一樣時,Dijkstra算法能夠很是方便的找出從地圖上某個起始區塊到其餘全部可達區塊的最短路徑,這裏仍然只考慮上下左右四個方向移動的狀況,算法流程以下:

說明:起始區塊記做S,從S到當前區塊G的總移動消耗記做CG,優先隊列openList中數據爲(G,CG)(區塊,S到當前區塊總移動消耗),區塊G自身移動消耗記做ZG。

  1. 設定起始區塊S,將區塊S和總移動消耗C=0(記做(S,0))放入openList,其中openList是一個優先隊列(PriorityQueue),總移動消耗C越低優先級越高;
  2. 判斷openList是否爲空,若是是空,算法結束;不然,從openList中拿出優先級最高的區塊G;
  3. 遍歷G的上下左右四個相鄰區塊N1-N4,對每一個區塊N,若是N已經在closeList中,忽略該區塊;若是N不可達,忽略該區塊;不然會有兩種狀況:
    1. 若是N不在openList中,那麼將(N,CN)放入openList中,其中CN=CG+ZN,既S到N的移動總消耗等於S到G的移動總消耗加上N自己的移動消耗,令N的父節點爲G;
    2. 若是N已經在openList中,取出(N,CN),仍然計算CN1=CG+ZN,若是CN1小於CN,用(N,CN1)替換openList中的(N,CN),令N的父節點爲G;若是CN1大於或等於CN,不作處理。
  4. 重複步驟2直至算法結束。

演示gif :

alt text

演示程序

藍色區域不可經過,白色區塊表明平原地形,移動一格消耗爲1,綠色區塊表明森林,移動一格消耗爲5,黑色區塊帶表山脈,移動一格消耗爲10。區塊中的數字表示從起始點到當前區塊的最小移動消耗。從演示程序能夠看出,因爲優先隊列的存在,區塊消耗越高,進入closeList的時間越靠後,這與廣度優先算法中一層層向外擴展的方式不一樣。

不難想到,當地圖上的全部區塊移動消耗相同時,Dijkstra算法就簡化爲廣度優先算法,由於移動總消耗最低的區塊總會是當前區塊的相鄰區塊。

在《Introduction to A*》中,做者提出了一個很是有趣的Dijkstra算法的應用,在這裏和你們分享下:在某個遊戲中,當我但願個人角色更傾向於通過某些區塊時(好比通過這些區塊能夠得到增益效果、道具等等)或者傾向於躲避某些區塊時(好比通過這些區塊會丟失生命值,或者這些區塊上的敵人很是危險),咱們能夠經過調整這些區塊的移動消耗來影響移動路徑的產生從而影響角色的移動行爲。一片區域上的移動消耗很小時,算法生成的最短路徑會傾向於通過這片區域,如gif中要到達森林區塊的右側時,路徑沒有橫穿森林,而是繞着森林邊緣走的,反之亦然。

廣度優先算法和Dijkstra算法都須要遍歷整個地圖,而在大多數場景中,咱們只須要知道一個點到另外一個點的最短路徑,下面的Greed-Best-First-Search爲咱們提供了一個思路。

網上沒有找到比較官方的翻譯,有人譯做「最好優先貪婪算法」,咱們暫時這麼稱呼它。

最好優先貪婪算法與上面兩種算法的不一樣之處在於,它老是嘗試向離目標節點更近的方向探索,怎樣纔算離目標節點更近呢?在只能上下左右四方向移動的前提下,咱們經過計算當前節點到目標節點的曼哈頓距離來進行判斷。

假設當前節點座標爲(x,y),目標節點的座標爲(x1,y1),曼哈頓距離計算公式以下:

Manhattan_distance = abs(x1-x)+abs(y1-y)

因爲曼哈頓距離只在兩點之間沒有障礙物的狀況下才與實際距離相等,通常狀況下曼哈頓距離老是小於實際距離。所以,當節點間不存在障礙物時,算法能夠保證找出最短路徑,可是一旦障礙物出現,最短路徑就沒法保證了。

算法流程以下:

說明:起始節點記做S,目標節點記做E,對於任意節點G,從當前節點G到目標節點E的曼哈頓距離記做MG,優先隊列openList中數據爲(G,MG)(節點,當前節點到目標節點E的曼哈頓距離)。

  1. 將起始節點S放入openList,openList是一個優先隊列,曼哈頓距離越小的節點,優先級越高。
  2. 判斷openList是否爲空,若是爲空,搜索失敗,目標節點E不可達;若是不爲空,從openList中拿出優先級最高的節點G;
  3. 遍歷節點G的上下左右四個相鄰節點N1-N4,若是N在openList或closeList中,忽略節點N;不然,令N的父節點爲G,計算N到E的曼哈頓距離MN,將(N,MN)放入openList。
  4. 判斷節點G是否是目標節點E,若是是,搜索成功,獲取節點G的父節點,並遞歸這一過程(繼續得到父節點的父節點),直至找到初始節點S,從而得到從G到S的一條路徑;不然,重複步驟2。

演示gif:

alt text

演示程序

演示程序中,暗藍色表示節點是障礙物,土黃色表示節點處於closeList中,淡藍色表示節點處於openList中,白色表示節點處於搜索出的結果路徑上。點擊地圖上的區塊能夠從新設置目標節點E。能夠看出,當目標節點處於地圖左下方時,搜索路徑很明顯不是最短路徑。雖然算法不能保證能夠找到最短路徑,但當地形不復雜時(如gif中起點和終點間沒有障礙物),路徑搜索速度是四種算法中最快的。

最好優先貪婪算法雖然不能保證找出最短路徑,但爲咱們提供了一個思路,A*算法就是Dijkstra算法與最好優先貪婪算法結合後獲得的算法。

A*算法


A*算法與最好優先貪婪算法同樣都經過計算一個值來判斷探索的方向。對於節點N,計算公式以下:

F(N)=G(N)+H(N)

其中G(N)就是Dijkstra算法中計算的,從起點到當前節點N的移動消耗,而H(N),在只容許上下左右移動的前提下,就是最好優先貪婪算法中當前節點N到目標節點E的曼哈頓距離。所以,當節點間移動消耗很是小時,G對F的影響也會微乎其微,A*算法就退化爲最好優先貪婪算法;當節點間移動消耗很是大以致於H對F的影響微乎其微時,A*算法就退化爲Dijkstra算法。

算法流程以下:

說明:起始節點記做S,目標節點記做E,對於任意節點P,從S到當前節點P的總移動消耗記做GP,節點P到目標E的曼哈頓距離記做HP,從節點P到相鄰節點N的移動消耗記做DPN,用於優先級排序的值F(N)記做FP。

  1. 選擇起始節點S和目標節點E,將(S,0)(節點,節點F(N)值)放入openList,openList是一個優先隊列,節點F(N)值越小,優先級越高。
  2. 判斷openList是否爲空,若爲空,則搜索失敗,目標節點不可達;不然,取出openList中優先級最高的節點P;
  3. 遍歷P的上下左右四個相鄰接點N1-N4,對每一個節點N,若是N已經在closeList中,忽略;不然有兩種狀況,
    • 若是N不在openList中,令GN=GP+DPN,計算N到E的曼哈頓距離HN,令FN=GN+HN,令N的父節點爲P,將(N,FN)放入openList;
    • 若是N已經在openList中,計算GN1= GP+DPN,若是GN1小於GN,那麼用新的GN1替換GN,從新計算FN,用新的(N,FN)替換openList中舊的(N,FN),令N的父節點爲P;若是GN1不小於GN,不做處理。
  4. 將節點P放入closeList中。判斷節點P是否是目標節點E,若是是,搜索成功,獲取節點P的父節點,並遞歸這一過程(繼續得到父節點的父節點),直至找到初始節點S,從而得到從P到S的一條路徑;不然,重複步驟2;

演示gif:

alt text

演示程序

演示gif中,土黃色表示節點在closeList中,淡藍色表示節點在openList中,深藍色表示節點不可經過,白色表示節點在搜索出的結果路徑上。能夠看出,A*算法老是設法保證搜索路徑上的F值保持不變。

在繼續測試演示程序時,我發現了一個問題:

alt text

因爲我用JS開發的上述演示程序,沒有趁手的優先隊列,因此用Array客串了一把,每次取值時根據F值進行倒序排序,取隊列末尾的值。從gif中能夠看出,算法執行的並不完美,咱們固然但願A*算法在簡單環境下可以擁有和最好優先貪婪算法同樣的運行速度,但個人A*算法卻出現了無用的掃描,並不在搜索結果上的區域也參與了計算。怎樣避免這一狀況呢?

首先想到的固然是改進個人優先隊列,但這有點麻煩啊,彆着急,有更簡單的方法,這個方法和接下來要了解的啓發式算法有關。

關於最優選擇貪婪算法和A*算法中的曼哈頓距離的運用屬於啓發式算法(Heuristic Algrathm)的一種,這也是A*算法公式F=G+H中H的由來。

這裏摘抄一段《Introduction to A*》的做者在另外一篇文章《Heuristic》中的一小段,講述H(n)如何影響A*算法的行爲。

At one extreme, if h(n) is 0, then only g(n) plays a role, and A* turns into Dijkstra’s algorithm, which is guaranteed to find a shortest path.
If h(n) is always lower than (or equal to) the cost of moving from n to the goal, then A* is guaranteed to find a shortest path. The lower h(n) is, the more node A* expands, making it slower.
If h(n) is exactly equal to the cost of moving from n to the goal, then A* will only follow the best path and never expand anything else, making it very fast. Although you can’t make this happen in all cases, you can make it exact in some special cases. It’s nice to know that given perfect information, A* will behave perfectly.
If h(n) is sometimes greater than the cost of moving from n to the goal, then A* is not guaranteed to find a shortest path, but it can run faster.
At the other extreme, if h(n) is very high relative to g(n), then only h(n) plays a role, and A* turns into Greedy Best-First-Search.

翻譯以下:

  1. 一種極端狀況是,當H(n)=0時,只有G(0)有效,此時A*算法變爲Dijkstra算法,能夠保證找到最短路徑。
  2. 若是H(n)總能保證不大於從n到終點的實際距離,那麼A*算法就能夠保證找到最短路徑(如上面演示程序中,在智能上下左右四方向移動的前提下,曼哈頓距離老是小於或等於實際距離)。H(n)相比實際距離越小,A*算法須要探索的節點就更多,性能就會更差一些。
  3. 若是H(n)與n到終點的實際距離相等,那麼A*算法就能夠一直保持探索路徑在最優路徑上而不須要探索額外的節點,使得算法執行很是快。雖然這是理想狀態下的場景,可是若是提早對地圖進行分析,咱們仍是能夠保證A*算法在近似理想的狀態下工做。(關於A*算法的優化後面會講到)
  4. 另外一種極端狀況是,當H(n)相對於G(n)很是大時,只有H(n)有效,A*算法就會變成最優選擇貪婪算法,不能保證找到最短路徑。

回到剛纔的問題,這個簡單的方法就是修改咱們的H(n),新的曼哈頓距離公式爲

Manhattan_distance = abs(x1-x)+abs(y1-y)*1.01

咱們對上下方向的距離進行了輕微的調整,效果如何呢?

alt text

演示程序

容許斜方向移動演示程序

能夠看到,掃描過程很是高效,全部closeList中的節點都出如今告終果路徑上。這是由於A*算法會沿着F值變小的方向搜索,因爲曼哈頓公式的調整,本來F值相等的節點再也不想等,同一列由上到下遞減,這就產生了gif中的現象,結果路徑老是先向下走,直到和目標節點同一行後,再向右走。

關於四種算法的選擇

  1. 雖然最優選擇貪婪算法只在特定狀況下才能夠找到最短路徑(沒有障礙物、沒有地形移動消耗差別),可是它的運行速度是最快的。若是狀況容許,優先使用本算法。
  2. 當須要知道地圖上某個點到全部其餘點的最短路徑,或者反過來,地圖上全部點到某個點的最短路徑時,選擇廣度優先算法(各區塊移動消耗相同)或Dijkstra算法(各區塊移動消耗不一樣)。
  3. 在通常狀況下,使用A*算法老是正確的。

A*算法的通常優化


在狀況容許的前提下,在生成地圖或者加載地圖時,記錄地圖上的特徵區域。特徵區域分爲兩類:

  • 第一類是不可到達區域,當目標點位於不可到達區域時,以上四類算法都會進行全圖掃描,這絕對是資源的極大浪費;
  • 第二類是導航點,所謂導航點,就是地圖上兩個區域間移動的必經之路,例如遊戲中兩片陸地被河流分割,中間一座小橋,這座橋就是導航點,當須要找到從一片大陸到另外一片大陸的最短路徑時,能夠先算出起點到這座橋的最短路徑,再算出這座橋到終點的最短路徑,那麼二者加起來就是起點到終點的最短路徑。一樣的道理,若是地圖分爲兩層,樓梯的部分也是導航點。
相關文章
相關標籤/搜索