A*尋路算法與它的速度

如果你是一個遊戲開發者,或者開發過一些關於人工智能的遊戲,你一定知道A*算法,如果沒有接觸過此類的東東,那麼看了這一篇文章,你會對A*算法從不知道變得了解,從瞭解變得理解。
我不是一個純粹的遊戲開發者,我只是因爲喜歡而研究,因爲興趣而開發,從一些很小的遊戲開始,直到接觸到了尋路等人工智能,纔開始查找一些關於尋路方面的文章,從而知道了A*算法,因爲對於初期瞭解的我這個算法比較複雜,開始只是copy而已,現在我們一起來精密的研究一下A*算法,以及提高它的速度的方法。
  一,A*算法原理
  我看過Panic翻譯的國外高手Patrick Lester的一篇關於A*算法初探的文章,現在我就根據回憶,來慢慢認識A*算法的原理。
我們先來看一張圖
  
  圖中從起點到終點,需要繞過一些遮擋,也許我們看的比較簡單,但是實際上,交給電腦來實現卻要經過一番周折,電腦如何知道哪裏有遮擋物,又是如何找到從起點到終點的最短路徑的呢?
瞭解這些,我們首先要知道一個公式:
F = G + H
其中,F 是從起點經過該點到終點的總路程,G 爲起點到該點的“已走路程”,H 爲該點到終點的“預計路程”。
A*算法,要從起點開始,按照它的算法,逐步查找,直到找到終點。
初期,地圖上的節點都是未開啓也未關閉的初始狀態,我們每檢測一個節點,就要開啓一些節點,檢測完之後,要把檢測完的節點,就要把它關閉。
我們需要一個開啓列表和關閉列表,用來儲存已經被開啓的節點和被關閉的節點。
這些就讓我們在實際過程中來深入瞭解吧。
看下面的圖
  
  首先,我們來從起點出發,開啓它周圍的所有點,因爲遮擋是無法通過的,我們不去管它,這樣,被我們開啓的節點,就是圖中的三個節點,它們的父節點就是起點,所以圖中的箭頭指向起點,計算相應的FGH值,如圖所視,檢測完畢,將起點放入關閉列表。
這個時候,我們從被開啓的所有節點中查找F值最小的節點,做爲下一次檢測的節點,然後開啓它周圍的點。
這時候起點左方和下方的F值都是70,我們根據自己的喜好選擇任意一個,這裏先選擇下方的節點進行檢測。
如下圖
  
  首先把未被開啓的剩下的節點的父節點指向檢測點。
已經開啓的點,我們不去開啓第二遍,但是我們計算一下從檢測點到達它們的新的G值是否更小,如果更小則代表目前的路徑是最優的路徑,那麼把這個節點的父節點改爲目前的檢測點,並重新計算這個點的FGH的值,全部檢測完畢之後,關閉檢測點,然後開始尋找下一個節點,如此循環,直到找到終點。
然後從終點開始,按照每個節點的父節點,倒着畫出路徑,如下圖
   
  
   
  這個就是A*算法的原理,說難倒是不難,但是對於初步接觸的人來說有點費勁而已。
   
  二,A*算法的速度
  前面,我們瞭解了A*算法的原理,發現,在每次查找最小節點的時候,我們需要在開啓列表中查找F值最小的節點,研究A*的速度,這裏就是關鍵,如何更快的找出這個最小節點呢?
  1,普通查找算法
  我們先來看看,最簡單的做法,就是每次都把開啓列表中所有節點檢測一遍,從而找到最小節點
  1. <i><i>private function getMin():uint {
  2. var len:uint = _open.length;
  3. var min:Object = new Object();
  4. min.value_f = 100000;
  5. min.i = 0;
  6. for (var i:uint = 0; i<len; i++) {
  7.   if (min.value_f>_open[i].value_f) {
  8.    min.value_f = _open[i].value_f;
  9.    min.i = i;
  10.   }
  11. }
  12. return min.i;
  13. }</i></i>
複製代碼
這裏我用了一張很簡單的地圖來驗證此方法
運行結果如圖
  
  我們看到,耗時38毫秒,其實這個數字是不準確的,我們權且當作參考
  2,排序查找算法
  顧名思義,這個算法就是,始終維持開啓列表的排序,從小到大,或者從大到小,這樣當我們查找最小值時,只需要把第一個節點取出來就行了
維持列表的排序,方法是在太多了,我的方法也許很笨,勉強參考一下吧,我們每次排序的同時,順便計算列表中的平均值,這樣插入新節點的時候,根據這個平均值來判斷從前面開始判斷還是從後面開始判斷
  //加入開放列表
  1. //加入開放列表
  2. private function setOpen(newNode:Object):void {
  3.         newNode.open = true;
  4.         var __len:int = _open.length;
  5.         if (__len==0) {
  6.                 _open.push(newNode);
  7.                 _aveOpen = newNode.value_f;
  8.         }else {
  9.                 //和F平均值做比較,決定從前面或者後面開始判斷
  10.                 if (newNode.value_f<=_aveOpen) {
  11.                         for (var i:int=0; i<__len; i++) {
  12.                                 //找到比F值小的值,就插在此值之前
  13.                                 if (newNode.value_f<=_open[i].value_f) {
  14.                                         _open.splice(i, 0, newNode);
  15.                                         break;
  16.                                 }
  17.                         }
  18.                 } else {
  19.                         for (var j:int=__len; j>0; j--) {
  20.                                 //找到比F值大的值,就插在此值之前
  21.                                 if (newNode.value_f>=_open[(j-1)].value_f) {
  22.                                         _open.splice(j, 0, newNode);
  23.                                         break;
  24.                                 }
  25.                         }
  26.                 }
  27.                 //計算開放列表中F平均值
  28.                 _aveOpen += (newNode.value_f-_aveOpen)/_open.length;
  29.         }
  30. }
  31. //取開放列表裏的最小值
  32. private function getOpen():Object {
  33.         var __next:Object =  _open.splice(0,1)[0];
  34.         //計算開放列表中F平均值
  35.         _aveOpen += (_aveOpen-__next.value_f)/_open.length;
  36.         return __next;
  37. }
複製代碼
運行結果如圖
  
  我們看到,耗時25毫秒,這個數字雖然不準確的,但是與普通查找算法相比較,速度確實是提高了
  3,二叉樹查找算法
(參考了火夜風舞的c++新霖花園中的文章)
這個算法可以說是A*算法的黃金搭檔,也是被稱爲苛求速度的binary heap”的方法
就是根據二叉樹原理,來維持開啓列表的“排序”,這裏說的排序只是遵循二叉樹的原理的排序而已,即父節點永遠比子節點小,就像下面這樣
   1
|    |
5    9
|   |  |
7  12 10
二叉樹每個節點的父節點下標 = n / 2;(小數去掉)
二叉樹每個節點的左子節點下標 = n * 2;右子節點下標 = n * 2 +1
注意,這裏的下標和它的值是兩個概念
  1. //加入開放列表
  2. private function setOpen(newNode:Object,newFlg:Boolean = false):void {
  3.         var new_index:int;
  4.         if(newFlg){
  5.                 newNode.open = true;
  6.                 var new_f:int = newNode.value_f;
  7.                 _open.push(newNode);
  8.                 new_index = _open.length - 1;
  9.         }else{
  10.                 new_index = newNode.index;
  11.         }
  12.         while(true){
  13.                 //找到父節點
  14.                 var f_note_index:int = new_index/2;
  15.                 if(f_note_index > 0){
  16.                         //如果父節點的F值較大,則與父節點交換
  17.                         if(_open[new_index].value_f < _open[f_note_index].value_f){
  18.                                 var obj_note:Object = _open[f_note_index];
  19.                                 _open[f_note_index] = _open[new_index];
  20.                                 _open[new_index] = obj_note;
  21.                                                                
  22.                                 _open[f_note_index].index = f_note_index;
  23.                                 _open[new_index].index = new_index;
  24.                                 new_index = f_note_index;
  25.                         }else{
  26.                                 break;
  27.                         }
  28.                 }else{
  29.                         break;
  30.                 }
  31.         }
  32. }
  33. //取開放列表裏的最小值
  34. private function getOpen():Object {
  35.         if(_open.length <= 1){
  36.                 return null;
  37.         }
  38.         var change_note:Object;
  39.         //將第一個節點,即F值最小的節點取出,最後返回
  40.         var obj_note:Object = _open[1];
  41.         _open[1] = _open[_open.length - 1];
  42.         _open.pop();
  43.         _open[1].index = 1;
  44.         var this_index:int = 1;
  45.         while(true){
  46.                 var left_index:int = this_index * 2;
  47.                 var right_index:int = this_index * 2 + 1;
  48.                 if(left_index >= _open.length){
  49.                         break;
  50.                 }else if(left_index == _open.length - 1){
  51.                         //當二叉樹只存在左節點時,比較左節點和父節點的F值,若父節點較大,則交換
  52.                         if(_open[this_index].value_f > _open[left_index].value_f){
  53.                                 change_note = _open[left_index];
  54.                                 _open[left_index] = _open[this_index];
  55.                                 _open[this_index] = change_note;
  56.                                                                
  57.                                 _open[left_index].index = left_index;
  58.                                 _open[this_index].index = this_index;
  59.                                                                
  60.                                 this_index = left_index;
  61.                         }else{
  62.                                 break;
  63.                         }
  64.                 }else if(right_index < _open.length){
  65.                         //找到左節點和右節點中的較小者
  66.                         if(_open[left_index].value_f <= _open[right_index].value_f){
  67.                                 //比較左節點和父節點的F值,若父節點較大,則交換
  68.                                 if(_open[this_index].value_f > _open[left_index].value_f){
  69.                                         change_note = _open[left_index];
  70.                                         _open[left_index] = _open[this_index];
  71.                                         _open[this_index] = change_note;
  72.                                                                        
  73.                                         _open[left_index].index = left_index;
  74.                                         _open[this_index].index = this_index;
  75.                                                                        
  76.                                         this_index = left_index;
  77.                                 }else{
  78.                                         break;
  79.                                 }
  80.                         }else{
  81.                                 //比較右節點和父節點的F值,若父節點較大,則交換
  82.                                 if(_open[this_index].value_f > _open[right_index].value_f){
  83.                                         change_note = _open[right_index];
  84.                                         _open[right_index] = _open[this_index];
  85.                                         _open[this_index] = change_note;
  86.                                                                        
  87.                                         _open[right_index].index = right_index;
  88.                                         _open[this_index].index = this_index;
  89.                                                                        
  90.                                         this_index = right_index;
  91.                                 }else{
  92.                                         break;
  93.                                 }
  94.                         }
  95.                 }
  96.         }
  97.         return obj_note;
  98. }
複製代碼
運行結果如圖
  
  我們看到,耗時15毫秒,速度是這三個方法裏最快的,但是因爲這個數字是不夠準確的,實際上,用二叉樹查找法,會讓A*算法的速度提高几倍到10幾倍,在一些足夠複雜的地圖裏,這個速度是成指數成長的。
  4,總結
得出結論,用了A*算法,就要配套的用它的黃金搭檔,二叉樹,它可以讓你的遊戲由完美走向更完美。

 

                                                   轉載請註明來自:http://www.shengshiyouxi.com

發佈了25 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章