關於路徑搜索算法的實用性優化

關於路徑搜索算法的實用性優化
UESTC 20013080 林 偉 2002.9.12

介紹:本文闡述對著名的路徑搜索算法A*算法的重要改進,使之更實用於大規模,高效率,多阻塞,模糊求解的任務中。希望本文起一個拋磚引玉的作用,使讀者能舉一反三。

  這裏所提及的A*算法在許多領域內得到廣泛的應用,比如我們熟悉的即時戰略遊戲正是利用這個算法來實現路徑搜索的。但是人工智能的書上只是說,卻很少有實現的例子,理論與實際差距太大,一些專業人士也曾經書寫過代碼,但代碼的優點在於說明算法,而在效率與實用性方面就有些欠缺。

A*是啓發試搜索加動態規劃。具體實現依靠兩個隊列Open隊列和Close隊列。從一點開始探走幾個相鄰的格子如果可以移動且當前移動爲起點到哪個格子的歷史最佳方法則把那個格子按照估價值從小到大插入Open隊列裏面,幾個方向試探結素後取出估價值最小的節點放入Close再從這裏開始試探幾個相鄰的方向同樣放入Open隊列裏面,放入Open的條件是1.這步在地圖上面是可以移動的,2.這步所在節點在Open裏面並不存在,3.從起點到這步的實際距離比這點的歷史最小距離還短滿足這三個條件就把節點放入Open隊列。具體的算法網友們已經描述的再清楚不過了大致算法如下:

 

WHILE TRUE BEGIN

1.      S點加入OPEN隊列(按該點到E點的距離排序+走過的步數從小到大排序)

2.      排序隊列OPEN隊列中距離最小的第一個點出列,並保存入CLOSE隊列中

3.      從出列的點出發,分別向4個(或8個)方向中的一個各走出一步

4.      估算第3步所走到位置到目標點的距離,並把該位置按估價距離從小到大排序後並放入OPEN中

5.      如果該點從四個方向上都不能移動,則把該點從CLOSE隊列中刪除

6.      從目標點回溯樹,直到樹根則可以找到最佳路徑,並保存在PATH中

END

圖表1:A*路徑搜索算法流程

 

具體實現和詳細算法可以看代碼 http://www.joynb.net/maker/qfind_c.htm (其中sort_queue和store_queue是open和close隊列)

  我覺得要使它可以勝任即時戰略遊戲第一點要改的就是規定搜尋的規模,即限制close_queue的大小,一旦超過大小而並沒有到達終點,則取一個搜尋過的最接近終點的點(從它到終點的估價距離最短)作爲搜索的終點。二一旦Open隊列空了無法取出節點時搜索結束沒有找到終點,此時還是按照上面的方法找一個最接近終點的點代替終點

這樣搜索就不會漫無邊際地進行下去了。上面的程序大家稍微觀察就會發現幾處影響速度的致命地方,啓發試搜索是不變的,而看程序每次加入取出兩個隊列時都要進行繁瑣的內存分配,這是項耗費時間的工作,其次需要檢查是否在隊列中,這點也是很慢的,最後就是保存動態規劃數據(歷史最短距離)的數組進行還原,並且每次尋路都要還原若大的數組,這是無法接受的。

我所說的優化是從數據結構入手解決上面的問題讓Open/Close兩隊列處理時不再涉及內存分配問題,首先建立一個與地圖上面每個節點一一對應的節點數組Node[maph][mapw];Node裏面有一個指針Next和Father,Next指相所在隊列的下一個節點,Father指向它的父節點。由於Open/Close兩隊列不可能出現一個節點在某個隊列同時出現兩次的情況因此Open隊列描述時只要讓它指向一個節點地址&Node[y][x]然後讓Node[y][x]的Next指向隊列下一個節點,如果沒有節點則這點的Next爲NULL,同樣的道理Close也是這樣描述的如此來看每次搜尋的時候就不必內存分配了,插入節點(x,y)時只要改動Node[maph][mapw]上面的數據和Open/Close兩個指針就輕鬆搞定了見程序的AddToOpenQueue等幾個函數。在初始化的時候一次性分配**Node爲Node[maph][mapw](TAstarNode的定義如下表)Open/Close爲兩個TAstarNode指針以後就不再分配內存了,初始化讓Open/Close都等於NULL在Open中加入節點(x,y)就按照普通連表的方法插入&Node[y][x]換而言之,就是讓Open/Close指向Node[maph][mapw]中的某個元素,然後這個元素又指向下一個元素形成鏈表,由於算法中每次加入Open/Close的節點前檢查如果存在了就不再加入因此Open/Close不會有重複的元素。
 

struct TAstarNode
{
 ADWORD Pos;     // ((y<<16)|x)初始化時設定以後不變
 short ActualCost;  // 保存從起點到該節點的實際開銷
 short EstimateCost; // 保存此點的估價開銷由JudgeCost函數提供初始化爲MAX
short SumCost; // 前面兩者的和,用於插入排序
 TAstarNode *Father; // 此點的父節點
 TAstarNode *Prev;  // 在Open或者Next鏈表的上一個節點用來方便從兩隊列中刪除
 TAstarNode *Next;  // 在Open或者Next鏈表中的下一個節點
 char Modified;    // 1位該節點是否被修改過,2/3位代表是否於Open/Close表
} **Node,*Open,*Close; // Node[maph][mapw]和Open/Close隊列

圖表2:算法接點數據結構

 

圖表3:算法搜索過程的圖形解釋  請看右邊的搜索試列圖,可以簡單看出OPEN隊列在插入/刪除時並不需要內存分配而只要改變其指針和Node[h][w]中節點的Next指針就行了。這是激動人心的提高

搜索過程示例

  上圖爲Node[h][w]和Open表的示範,初始化Node時設置EstimateCost爲MAX另外需要一個變量描述改動狀態(Modified),如果改動過最佳記錄則狀態第0位爲1,如果在Open隊列中則地1位爲1,如果在Close中則第2位爲1那麼只要通過位運算就可以知道節點和隊列的關係了,另外用一個數組來記錄修改過最佳距離的節點位置,並且依據前面的狀態變量來判斷如果以前已經記錄過就不再記錄了,當路徑搜索完以後就根據這個數組來還原數據了。

  請看上邊的搜索試列圖,可以簡單看出OPEN隊列在插入/刪除時並不需要內存分配而只要改變其指針和Node[h][w]中節點的Next指針就行了。這是激動人心的提高,

  我們首先加入了搜索限制其次除去了內存分配,再者把每次查找一個節點是否在Open/Close鏈表中從搜索鏈表化簡成檢查標誌,路徑算法已經省去了許多不必要的工序了,再看下實用性方面,首先判斷地圖是否可以移動不要在算法中直接判斷而最好設置一個虛函數MoveAble(x,y)來實現(程序中我用函數指針代替),因爲在遊戲中某塊地圖對於地面單位和空中單位還有水下單位可以移動的情況都不同因此直接操作地圖元素並不是明智的選擇,再者估價函數JudgeCost(x,y)也應該設置成虛函數或者函數指針來實現,畢竟求估價值的方法很靈活,這裏不能限制死了。

  還有每次尋路搜索哪些方向呢?用DirMask來描述0-7位代表從上開始順時針一圈的八個方向情況,0代表不搜索,1代表搜索,那麼四個方向搜索可以用0x55(1010101b)來代替,八個方向可以用0xff來設定,如此一來你可以設定只進行搜索3個方向的尋路,這樣可以滿足一些簡單的尋路任務了,比如RPG中NPC敵人的移動就可以這樣來設定。最後就是範圍問題的設定了,Open表的最大大小可以設置爲100-200(四個方向)或200-300(八個方向)而Close表的大小即處理節點的最多數目可以設置成(MapH*MapW)/16這樣的設定在士兵走入一個桶形地形時並不會花費很大的開銷去尋找通路,而是走到桶形地形的尖端。其具體值可以適當調整,當然數值越大搜索就越廣,開銷也就越大(MapH*Map)/16這個數值我自己感覺比較接近星際的尋路特點

  其它優化,由於Open隊列中的數據全部是連表插入排序的,當數據多時效率並不會見得高。這時如果還要繼續優化手段也很多,比如建立Hash散列表,或者用二叉樹來代替連表排序,大家可以參考前導公司的寫的A*代碼(http://www.joynb.net/maker/path.zip)裏面就用了散列表這點很優秀,但它任然擺脫不了大量的內存分配/釋放。因此此處我覺得不怎麼高明。

  到是估價值的計算方法很多,但是寫的好壞也關係到搜索的規模:座標差之絕對值乘加權值求和,平方和開方還有些怪的呢,可以看看前導的代碼估價函數是怎麼寫的。我看到網上有文章說當前節點到終點的估計距離不能比實際距離大,說什麼才能叫做A*算法,否則只能叫A算法(人工智能書上說的),因爲不一定是最短路徑,呵呵但是如果只是滿足估價值小於真實值也不一定就可以找到最短路徑故暫且不論,先看JudgeCost的意義--使搜索更靠近終點,勢必減小JudgeCost將會增加搜索的寬度及規模,增加JudgeCost則使搜索跟快地向終點靠攏。下面是使用不同估價函數的對比:

圖表4:搜索代價之JudgeCost=abs(dx)+abs(dy)

 

圖表5:搜索代價之JudgeCost=abs(dx),abs(dy)加權求和

上圖兩種估價函數的求法,星號是路徑點是搜索過的節點,當JudgeCost大於真實值時從一個節點出發算法更願意搜索一個靠終點近的點,換言之JudgeCost越大算法就越買力向終點靠攏,從左右兩副圖可以清楚看到(點擊放大)。所以我們使用座標差加權求和來處理或者直接求和乘與一個數字(比如8)在地形複雜的時候就更加顯出這種方法的實用性了,良種式子的規模差距還要比上圖嚴重的多。所以通用地用加權求和dx=abs(x-endx),dy=abs(y-endy);if (dx>dy) result =10*dx+6*dy; else result=10*dy+6*dx;就是這個意思,所用的乘10和乘6是試驗證明比較好用的數字了,如此一來搜索的規模就被控制得又減小了,是不可忽視的。這裏有例子程序(http://www.joynb.net/maker/PathFind.zip),可以測試搜索規模等。

  如果我們把它應用到即時戰略類遊戲中,還有幾點要注意的,集體尋路要採用跟隨政策,共用一條路徑,中途遇到移動物分三步走:嘗試加入一個斜方的路徑節點看是否可以饒過;不能就等待移動物體自己走開;到了時間不走再另外搜尋路徑,大場景要預先設定一些路徑節點中轉站再進行遠距離搜索時可以大大提高效率,用方向保存路徑比用座標保存節省至少1/4的空間。。。。碰到敵人部隊就開槍,自己部隊則可以不走了,或者發送兩條指令給擋住路的士兵的指令隊列1.移開當前位置並等待;2.回到當前位置,這樣可以產生一個一個順序讓路的精彩效果,紅警裏面早已有實現:-)

講遠了,那是行軍問題了,一年前我開始研究此算法,曾寫了一段三百多行的程序以及配合的說明文,如今拿出來增改一番。一家之言,歡迎來信討論: [email protected]

 

 

電子科技大學通訊工程學院20013080班
林偉2002年9月27日星期五

參考文獻:

1.     《人工智能技術導論》西安電子科大 ISBN7-5606-0811-6/TP.0417

2.    《精簡的A*算法》 http://www.joynb.net/maker/qfind_c.htm

3.    《人性化的尋路技術》http://mays.soage.com/develop/ai/200204/RealPath.htm

4.    Amit’s A* Pages”http://theory.stanford.edu/~amitp/GameProgramming/

5.    “Intelligent PF” http://www.gamasutra.com/features/19970801/pathfinding.htm

6.    “The Game AI Page” http://www.gameai.com/

本文電子版和源程序:http://www.joynb.net/maker/astar.htm

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章