1. 程式人生 > >針對初學者的A*演算法入門詳解(附帶Java原始碼)

針對初學者的A*演算法入門詳解(附帶Java原始碼)

      英文題目,漢語內容,有點掛羊頭賣狗肉的嫌疑,不過請不要打擊我這顆想學好英語的心。當了班主任我才發現大一18本書,11本是英語的,能多用兩句英語就多用,個人認為這樣也是積累的一種方法。

     Thanks open source pioneers dedicated to computer science especially A*.

一、演算法簡介

     為什麼寫這個,以前學長就用這個結合MFC做了個小遊戲,分為三個等級“弱智,一般,大神”,分別採用不同演算法來尋找迷宮出口,其中大神就是採用A*演算法,當時感覺好神奇啊。前幾天網上看到有人問A*演算法,我就研究了下。下面的這部分內容估計奔走在各大高校的人工智慧課上(國外的一片演算法分析),很明顯,筆者也不能免俗。

      我知道下面不屬於學術不端(沒發表,結合自己的理解重新表達),現在就一起共享大神的作品吧(從圖中可以看出並不是所有點都搜尋一遍才找到了路徑,給圖片的目的是為了直觀理解,以後忘記的話,看看圖片也就記起來演算法了,作為一名中共黨員,筆者只信仰馬克思主義,實踐檢驗真理的唯一標準)。

      我們假設某個人要從A點到達B點,而一堵牆把這兩個點隔開了,如下圖所示,綠色部分代表起點A,紅色部分代表終點B,藍色方塊部分代表之間的牆。

                                    ASC7Q(1[H3[8`C9F[H$WH$G       ]9Y1RQ_M~DNW[A8CED]BH4F

                           HVC95`I23RF{R~F9)G77NSNUR}[0[2HI1(41_N8{DYJNV7

      你首先會注意到我們把這一塊搜尋區域分成了一個一個的方格(如果地圖特別大,類似稀疏矩陣的話就要結合資料結構的稀疏矩陣了,劃分大塊,不過沒遇到過例子,不會),如此這般,使搜尋區域簡單化,正是尋找路徑的第一步。這種方法將我們的搜尋區域簡化成了一個普通的二維陣列。陣列中的每一個元素表示對應的一個方格,該方格的狀態被標記為可通過的和不可通過的。通過找出從A點到B點所經過的方格,就能得到AB之間的路徑。當路徑找出來以後,這個人就可以從一個格子中央移動到另一個格子中央,直到抵達目的地。  這些格子的中點叫做節點。當你在其他地方看到有關尋找路徑的東西時,你會經常發現人們在討論節點。為什麼不直接把它們稱作方格呢?因為你不一定要把你的搜尋區域分隔成方塊(感覺高大上),矩形、六邊形或者其他任何形狀都可以。況且節點還有可能位於這些形狀內的任何一處呢(這句不懂,莫非是每個大塊提前做一些預處理)?在中間、靠著邊,或者什麼的。我們就用這種設定,因為畢竟這是最簡單的情況。

      開始搜尋,當我們把搜尋區域簡化成一些很容易操作的節點後,下一步就要構造一個搜尋來尋找最短路徑。在A*演算法中,我們從A點開始,依次檢查它的相鄰節點,然後照此繼續並向外擴充套件直到找到目的地。 從A點開始,將A點加入一個專門存放待檢驗的方格的“開放列表”中。這個開放列表有點像一張購物清單。當前這個列表中只有一個元素(個人感覺可以不加),但一會兒將會有更多。列表中包含的方格可能會是你要途經的方格,也可能不是。總之,這是一個包含待檢驗方格的列表。檢查起點A相鄰的所有可達的或者可通過的方格,不用管牆啊,水啊,或者其他什麼無效地形,把它們也都加到開放列表中(這句話我很不認同,可能我理解錯了,看我程式碼你會發現,我最外圈加了一圈1,表示牆壁,遇到牆壁就繼續找其他相鄰節點,並沒有加入openTable)。對於每一個相鄰方格,將點A儲存為它們的“父方格”(類似最短路徑演算法列印路徑,當然你可以選擇嚴蔚敏老師的三維陣列儲存路徑法,關於SP問題請參考筆者的(包括四大演算法)

http://www.cnblogs.com/hxsyl/p/3270401.html)。當我們要回溯路徑的時候,父方格是一個很重要的元素。從開放列表中去掉方格A,並把A加入到一個“封閉列表”中。封閉列表存放的是你現在不用再去考慮的方格(說的很清楚,只是暫時不用考慮,如果不進行二次判斷(指的是重新拿出來這個點來更新周邊)的話,幹嘛要closeTable,所以筆者認為不要光看理論,想一下實際情況,很多問題就會豁然開朗)。此時你將得到如圖所示的樣子。在這張圖中,中間深綠色的方格是你的起始方格,所有相鄰方格目前都在開放列表中,並且以亮綠色描邊。每個相鄰方格有一個灰色的指標指向它們的父方格,即起始方格。然後排序找到G最小的點作為下次起點。

      下面還有很多,感覺不必贅述,隨便百度一下都有的。

二、演算法描述

  1: //這段虛擬碼可以看出個大概,但是不完全,知道意思就行
  2: while (Open表非空)  {    
  3:   從Open中取得一個節點X,並從OPEN表中刪除。   
  4:   if (X是目標節點)   {     
  5:     求得路徑PATH;   
  6:     返回路徑PATH;   
  7:   }    
  8:   for (每一個X的子節點Y)   {     
  9:     if (Y不在OPEN表和CLOSE表中)    {      
 10:       求Y的估價值;     
 11:       並將Y插入OPEN表中;    
 12:     }else if (Y在OPEN表中)    {      
 13:       if (Y的估價值小於OPEN表的估價值)      
 14:         更新OPEN表中的估價值;    
 15:       }    
 16:     else {//Y在CLOSE表中          
 17:       if (Y的估價值小於CLOSE表的估價值)     {       
 18:         更新CLOSE表中的估價值;       
 19:         從CLOSE表中移出節點,並放入OPEN表中;    
 20:       }    
 21:     }  
 22:   }
 23:   將X節點插入CLOSE表中;     
 24:   按照估價值將OPEN表中的節點排序;
 25: }

三、演算法Java實現

      看了兩天,感覺很簡單,真正寫的時候你會發現有多蛋疼,如果你是個愛思考的人,估計問題更多。

  1: package util;
  2: 
  3: import java.util.ArrayList;
  4: import java.util.Collections;
  5: import java.util.Stack;
  6: 
  7: public class AstarPathFind {
  8:   // 前四個是上下左右,後四個是斜角
  9:   public final static int[] dx = { 0, -1, 0, 1, -1, -1, 1, 1 };
 10:   public final static int[] dy = { -1, 0, 1, 0, 1, -1, -1, 1 };
 11: 
 12:   // 最外圈都是1表示不可通過
 13:   final static public int[][] map = {
 14:       { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
 15:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 16:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 17:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 18:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 19:       { 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1 },
 20:       { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
 21:       { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
 22:       { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
 23:       { 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1 },
 24:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 25:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 26:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 27:       { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
 28:       { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } };
 29: 
 30:   public static void main(String[] args) {
 31:     // TODO Auto-generated method stub
 32:     Point start = new Point(1, 1);
 33:     Point end = new Point(10, 13);
 34:     /*
 35:      * 第一個問題:起點FGH需要初始化嗎?
 36:      * 看參考資料的圖片發現不需要
 37:      */
 38:     Stack<Point> stack = printPath(start, end);
 39:     if(null==stack) {
 40:       System.out.println("不可達");
 41:     }else {
 42:       while(!stack.isEmpty()) {
 43:         //輸出(1,2)這樣的形勢需要重寫toString
 44:         System.out.print(stack.pop()+" -> ");
 45:       }
 46:       System.out.println();
 47:     }
 48: 
 49:   }
 50: 
 51:   public static Stack<Point> printPath(Point start, Point end) {
 52:     
 53:     /*
 54:      * 不用PriorityQueue是因為必須取出存在的元素
 55:      */
 56:     ArrayList<Point> openTable = new ArrayList<Point>();
 57:     ArrayList<Point> closeTable = new ArrayList<Point>();
 58:     openTable .clear();
 59:     closeTable.clear();
 60:     Stack<Point> pathStack = new Stack<Point>();
 61:     start.parent = null;
 62:     //該點起到轉換作用,就是當前擴充套件點
 63:     Point currentPoint = new Point(start.x, start.y);
 64:     //closeTable.add(currentPoint);
 65:     boolean flag = true;
 66:     
 67:     while(flag) {
 68:       for (int i = 0; i < 8; i++) {
 69:         int fx = currentPoint.x + dx[i];
 70:         int fy = currentPoint.y + dy[i];
 71:         Point tempPoint = new Point(fx,fy);
 72:         if (map[fx][fy] == 1) {
 73:           // 由於邊界都是1中間障礙物也是1,,這樣不必考慮越界和障礙點擴充套件問題
 74:           //如果不設定邊界那麼fx >=map.length &&fy>=map[0].length判斷越界問題
 75:           continue;
 76:         } else {
 77:           if(end.equals(tempPoint)) {
 78:             flag = false;
 79:             //不是tempPoint,他倆都一樣了此時
 80:             end.parent = currentPoint;
 81:             break;
 82:           }
 83:           if(i<4) {
 84:             tempPoint.G = currentPoint.G + 10;
 85:           }else {
 86:             tempPoint.G = currentPoint.G + 14;
 87:           }
 88:           tempPoint.H = Point.getDis(tempPoint,end);
 89:           tempPoint.F = tempPoint.G + tempPoint.H;
 90:           //因為重寫了equals方法,所以這裡包含只是按equals相等包含
 91:           //這一點是使用java封裝好類的關鍵
 92:           if(openTable.contains(tempPoint)) {
 93:             int pos = openTable.indexOf(tempPoint );
 94:             Point temp = openTable.get(pos);
 95:             if(temp.F > tempPoint.F) {
 96:               openTable.remove(pos);
 97:               openTable.add(tempPoint);
 98:               tempPoint.parent = currentPoint;
 99:             }
100:           }else if(closeTable.contains(tempPoint)){
101:             int pos = closeTable.indexOf(tempPoint );
102:             Point temp = closeTable.get(pos);
103:             if(temp.F > tempPoint.F) {
104:               closeTable.remove(pos);
105:               openTable.add(tempPoint);
106:               tempPoint.parent = currentPoint;
107:             }
108:           }else {
109:             openTable.add(tempPoint);
110:             tempPoint.parent = currentPoint;
111:           }
112: 
113:         }
114:       }//end for
115:       
116:       if(openTable.isEmpty()) {
117:         return null;
118:       }//無路徑
119:       if(false==flag) {
120:         break;
121:       }//找到路徑
122:       openTable.remove(currentPoint);
123:       closeTable.add(currentPoint);
124:       Collections.sort(openTable);
125:       currentPoint = openTable.get(0);
126:       
127:     }//end while
128:     Point node = end;
129:     while(node.parent!=null) {
130:       pathStack.push(node);
131:       node = node.parent;
132:     }    
133:     return pathStack;
134:   }
135: }
136: 
137: class Point implements Comparable<Point>{
138:   int x;
139:   int y;
140:   Point parent;
141:   int F, G, H;
142: 
143:   public Point(int x, int y) {
144:     super();
145:     this.x = x;
146:     this.y = y;
147:     this.F = 0;
148:     this.G = 0;
149:     this.H = 0;
150:   }
151: 
152:   @Override
153:   public int compareTo(Point o) {
154:     // TODO Auto-generated method stub
155:     return this.F  - o.F;
156:   }
157: 
158:   @Override
159:   public boolean equals(Object obj) {
160:     Point point = (Point) obj;
161:     if (point.x == this.x && point.y == this.y)
162:       return true;
163:     return false;
164:   }
165: 
166:   public static int getDis(Point p1, Point p2) {
167:     int dis = Math.abs(p1.x - p2.x) * 10 + Math.abs(p1.y - p2.y) * 10;
168:     return dis;
169:   }
170: 
171:   @Override
172:   public String toString() {
173:     return "(" + this.x + "," + this.y + ")";
174:   }
175:   
176: }
177: /*
178: 成功了,我在想找到的一定是最佳路線麼,別告訴我因為每次取最佳點,我的意思是可能8次每迴圈完就break了,男刀這是不同路徑的最佳路線
179: */
180: 

四、結束語

      大神提到,不管地圖差異的話,主要是排序耽誤時間,可考慮二叉堆(大神和我想法一樣,哈哈),實際就是堆排序(不太清楚的請參考博主這篇博文http://www.cnblogs.com/hxsyl/p/3244756.html),不過這都不是咱麼考慮的問題啦。。。。。好啦,洗洗該去上聽力課了。