無心中在cocoaChina的首頁看到了一篇介紹A*算法用swift實現的文章,對A*尋路算法產生了興趣。在百度谷歌了不少文章後,終於A*算法的流程,同時讓我發現了兩篇很是好的英文文章:html
A* Pathfinding for Beginnersjava
第一篇文章是很是好的A*算法入門文章,通讀一遍就基本能夠用代碼實現了;第二篇文章能夠說給我帶來了震撼,原來算法能夠這樣講,推薦你們都看一下。算法
看完第二篇文章就產生了要學習做者的方式講一下A*算法的衝動,同時也當是練練手,很久沒寫javaScript了。swift
講解方式也按照<Introduction to A*>一文中的順序,從最簡單的廣度優先算法(Breadth-First-Search)、大名鼎鼎的Dijkstra算法到Greed-Best-First-Search算法,最後是A*算法。app
(關於A*算法,網上資料不少,A*算法的變種也不少,有興趣的朋友能夠自行搜索,
本文僅對四種常見尋路算法進行簡單介紹,如有不合理或錯誤之處,請諒解並在回覆中指出)性能
廣度優先算法是最簡單的尋路算法,算法執行的結果是得到從地圖上任意一點S到其餘全部可達點的最短路徑,這裏只考慮上下左右四方向行走的狀況,算法流程很是容易理解:學習
演示gif:測試
演示程序優化
藍色方塊是不可經過的,S爲掃描的起始點,一層層向外擴展,最終全部可到達的節點都被掃描,這一過程有時被稱爲「flood fill」。對於每個被掃描的節點,爲其添加一個指向父節點的方向箭頭,而後你會發現,從地圖上任意一點開始,只要沿着箭頭的方向移動,總能走到起始點S,並且走過的路徑必然是最短路徑之一。
看到廣度優先算法,最早想到的應用場景就是塔防,敵人老是從固定的一個或幾個出生點出現,向着固定的一個或幾個目標移動,咱們徹底能夠在每一關開始前以出生點爲起始點遍歷整個地圖,這樣本關中怪物的移動路線就能夠肯定了。
下面讓咱們考慮如下場景,地圖中存在森林、山嶺和平原,角色在這些地形上移動時,移動力消耗是不一樣的,好比《文明》中。這就要求咱們把每個區塊的消耗考慮在內,這時,Dijkstra算法就能夠發揮做用了。
在地圖內的每一個區塊移動消耗不一樣時,Dijkstra算法能夠很是方便的找出從地圖上某個起始區塊到其餘全部可達區塊的最短路徑,這裏仍然只考慮上下左右四個方向移動的狀況,算法流程以下:
說明:起始區塊記做S,從S到當前區塊G的總移動消耗記做CG,優先隊列openList中數據爲(G,CG)(區塊,S到當前區塊總移動消耗),區塊G自身移動消耗記做ZG。
演示gif :
藍色區域不可經過,白色區塊表明平原地形,移動一格消耗爲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的曼哈頓距離)。
演示gif:
演示程序中,暗藍色表示節點是障礙物,土黃色表示節點處於closeList中,淡藍色表示節點處於openList中,白色表示節點處於搜索出的結果路徑上。點擊地圖上的區塊能夠從新設置目標節點E。能夠看出,當目標節點處於地圖左下方時,搜索路徑很明顯不是最短路徑。雖然算法不能保證能夠找到最短路徑,但當地形不復雜時(如gif中起點和終點間沒有障礙物),路徑搜索速度是四種算法中最快的。
最好優先貪婪算法雖然不能保證找出最短路徑,但爲咱們提供了一個思路,A*算法就是Dijkstra算法與最好優先貪婪算法結合後獲得的算法。
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。
演示gif:
演示gif中,土黃色表示節點在closeList中,淡藍色表示節點在openList中,深藍色表示節點不可經過,白色表示節點在搜索出的結果路徑上。能夠看出,A*算法老是設法保證搜索路徑上的F值保持不變。
在繼續測試演示程序時,我發現了一個問題:
因爲我用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.
翻譯以下:
回到剛纔的問題,這個簡單的方法就是修改咱們的H(n),新的曼哈頓距離公式爲
Manhattan_distance = abs(x1-x)+abs(y1-y)*1.01
咱們對上下方向的距離進行了輕微的調整,效果如何呢?
能夠看到,掃描過程很是高效,全部closeList中的節點都出如今告終果路徑上。這是由於A*算法會沿着F值變小的方向搜索,因爲曼哈頓公式的調整,本來F值相等的節點再也不想等,同一列由上到下遞減,這就產生了gif中的現象,結果路徑老是先向下走,直到和目標節點同一行後,再向右走。
關於四種算法的選擇
在狀況容許的前提下,在生成地圖或者加載地圖時,記錄地圖上的特徵區域。特徵區域分爲兩類: