出處:http://www.cnblogs.com/bigmonkey
本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途!
掃描二維碼關注公衆號「我是8位的」
假設地圖上有一片樹林,坦克須要繞過樹林,走到另外一側的軍事基地,在無數條行進路線中,哪條纔是最短的?node
這是典型的最短尋徑問題,可使用A*算法求解。A*搜索算法俗稱A星算法,是一個被普遍應用於路徑優化領域的算法,它的行爲的能力基於啓發式代價函數,在遊戲的尋路中很是有用。算法
A*算法的第一個步是將地圖表格化,具體來講是用一個大型的二維列表存儲地圖數據。這有點相似於像素畫:函數
畫中的小狗是由一個個像素方格組成的,方格越小,圖案越平滑。在坦克尋徑問題中,坦克的個頭遠小於地圖,所以咱們把坦克做爲一個像素,這樣一來,地圖就能夠切分爲一個個方格,其中S表明坦克的起點,E表明基地:post
咱們把地圖映射到二維列表上,每一方格均可以用惟一的二元組表示,元組的第一個維度是行號,第二個是列號,起點和終點的座標分別是(3,2)和(5,7)。「找到坦克的最短路徑」實際是在回答最短路徑須要通過那些方格。學習
A*算法的核心是一個評估函數:F(n)=H(n)+G(n)。優化
H(n)是距離評估函數,n表明地圖上的某一個方格,H(n)的值是該方格到終點的距離。距離的計算方式有不少,選擇不一樣的方式,計算的結果也不一樣:spa
假設每一個方格的邊長都是1,若是用歐幾里德距離S到計算S到E的距離,則:3d
若是用曼哈頓距離計算,則:code
G(n)是從起點移動到n的代價函數,n離起點越遠,付出的代價越高。起點達到n的路線有多條,每條路線的G值可能不一樣:orm
坦克從S到T的路線有兩條,S→A→B→C→T和S→D→T,第二條路線更短,付出的代價也更低。假設從一個方格移動到相鄰方格的代價是1,則G(D)=G(A)=1。B的前一步是A,所以G(B)=G(A)+1=2。同理,G(C)=G(B)+1=3。對於G(T)來講,它的值取決於T的上一步,若是路線是S→A→B→C→T,則G(T)=G(C)+1=4;若是路線是S→D→T,則G(T)=G(D)+1=2。值得注意的是,代價函數並非惟一的,具體如何定義,徹底取決於你本身。
某個位置的評估函數F僅僅是將該點的距離估值和代價值加起來。A*搜索的每個尋徑都會尋找評估值最小的點。
A*搜索涉及到兩個重要的列表,openList(開放列表,存儲候選節點)和closeList(關閉列表,存儲已經走過的節點)。算法先把起放入openList中,而後重複下面步驟:
1. 遍歷openList,找到F值最小的那個做爲當前所在節點,用P表示;
2. 把P加入closeList中,做爲已經走過的節點;
3. 探索P周圍相鄰且不在closeList中的每個節點,記算它們的H值、G值和F值,並把P設置爲這些方格的父節點,將這些節點做爲待探索節點添加到Q中。固然,如何定義「相鄰」也是你說的算。
4. 若是Q中的節點不在openList中,將其加入到openList。Q中的節點已經存在於openList中,比較這些節點的F值和它們在openList中的F值哪一個更小(F越小說明這條路徑越短),若是openList中的F值更小或兩者相等,不作任何改變,不然用Q中的節點替換掉openList中的節點。
5. 若是終點在openList中,退出,最短路徑就是從終點開始,沿着父節點移動直至起點;若是openList是空的,退出,此時意味着起點到終點沒有任何路可走。
彷佛不那麼直觀,咱們仍然以坦克移動的例子審視這個過程。
在遊戲開始之氣,先要制定一些遊戲規則。
坦克可每一步均可以移動到與之相鄰的八個方格中,咱們指定每個方格的邊長是10,從一個方格移動到相鄰方格的代價是這兩個方格中心點的距離。如此一來,坦克上、下、左、右平移一格所花費的代價是10(這裏之因此將邊長定義爲10而不是1,目的是爲了不向斜對角移動時產生小數),向斜對角移動的代價是:
下一步定義相鄰的方格是否可以探索。若是坦克的相鄰方格是障礙物,那麼坦克沒法移動到障礙物上,也沒法貼着障礙物移動到斜對角的方格
不能移動到×所在的方格
定義了遊戲規則後就能夠開始移動坦克。
咱們定義地圖是一個8×8的小地圖,使用曼哈頓距離做爲距離評估函數。以探索起點正上方的方格爲例,它的位置是(4,2),到起點的代價是G=10。
對於任意方格到終點的距離,咱們不考慮障礙物,僅僅是簡單的根據曼哈頓距離的公式計算。起點到終點的距離:
H(n) = H(4,2) = (|4 - 5| + |2 - 7|) * 10 = 60
這裏乘以了係數10,這是因爲咱們在遊戲規則中定義了方格的單位長度是10。
這有點相似於手機導航中的紅色連線,這條連線僅僅鏈接了車標和終點,並不考慮中間是否有阻礙物:
起點的G值是0,F=G+H=70。在待探索的八個方格中,咱們設置從上到下的三個數值分別表明G、H、F,使用一個箭頭指向是它的parent,箭頭的指向不一樣,G值也可能不一樣:
將S周圍的方格設置爲待探索方格
因爲openList是空的,因此把 Q 中的8個待探索節點都放入openList中。此時的openList中,F(4,3) 最小,所以選擇(4,3)做爲下一個到達的位置,並把它從openList移至closeList
有八個方格與(4,3)相鄰,其中(3,2)已經在closeList中,將它排除,(5,4)是障礙物,也排除,如今還剩六個,把它們都放入Q中:
將(4,3)相鄰的可探索方格放到Q中
在Q的六個點中,(5,2),(5,3),(4,4),(3,4)是第一次探索,直接加入到openList中;(4,2),(3,3)已經存在於openList中,表示兩者曾經被探索過。因爲是從(4,3)探索(4,2)和(3,3),所以兩者的G值與從S點探索時的G值不一樣,即GQ(4,2)≠GopenList(4,2),GQ(3,3)≠GopenList(3,3),而且它們的父節點也不一樣。很明顯,對於從S到(4,2)的兩條路徑來講,S→(4,3) →(4,2)要比S→ (4,2)更長,移動的代價更高,即GQ(4,2)> GopenList(4,2);同理,GQ(3,3)>GopenList(3,3)。此時保留(4,2)和(3,3)在openList中的的數值和箭頭指向:
保持openList中的(4,2)和(3,3)不變
如今,openList中(5,3)和(4,4)的F值都是64,選擇哪一個都無所謂,這徹底取決你本身制定的選取規則。這裏咱們用「胡亂選一個」的規則選擇了(4,4)做爲下一個目的地。與(4,4)相鄰的八個方格中,四個是障礙物,一個在closeList中,還剩下(5,3),(3,3),(3,4)。根據遊戲的規則,坦克沒法「貼着障礙物移動到斜對角的方格」,所以(5,3)也要從待探索方格中去掉:
從(4,4)出發,可探索(3,3)和(3,4)
Q中的F(3,3)和F(3,4)都大於OpenList中的F(3,3)和F(3,4),所以保留openList的元素不變:
保留openList的(3,3)和(3,4)
如今,openList的最小F值是F(5,3)=64,而(5,3)並不在Q中,說明對於路徑S→(4,3)→(4,4)的探索失敗了,但這並不妨礙咱們從openList中挑選最小值F(5,3)=64。根據遊戲規則,(5,3)周圍有4個可供探索的方格:
從(5,3)出發,可探索(4,2), (5,2), (6,2), (6,3)
相似地,Q中的F(4,2)和F(5,2)都小於openList中的F(4,2)和F(5,2),所以保持openList中的元素不變,將Q中的另外兩個元素(6,2)和(6,3)移至openList中:
保持openList中的(4,2)和(5,2)不變,添加(6,2)和(6,2)
在openList中,(4,2)是最佳選擇,而(4,2)並無指向(5,3),說明經過S→(4,3)→(5,3)並不能產生最佳路徑。
這個結論不妨礙繼續執行A*搜索,再一次從openList中選擇F值最小的元素(4,2)繼續探索
從(4,2)出發,可探索(3,1),(4,1),(5,1),(5,2)
在這一次探索中,Q中的最小F值F(5,2)=70已經小於openList中的F(5,2)=78,所以用Q中的(5,2)替換openList中的(5,2),這將從新改變(5,2)的評估值和父節點:
用Q中的(5,2)替換openList中的(5,2)
接下來從openLIst中選擇(5,2)做爲出發點,它周圍可探索(4,1),(5,1),(6,1),(6,2),(6,3)這5個方格:
從(5,2)出發,可探索(4,1),(5,1),(6,1),(6,2),(6,3)
此次openLIst中的最小F值是F(3,3)=70。選擇(3,3)後將會繼續選擇(3,4),此時咱們將又一次面對openLIst中有多個最小F值相等的狀況:
openList中多個最小F值相等,F(4,1)=F(5,1) = F(6,3)=F(2,4)=F(2,3) =84
不管選擇哪個,最終都將獲得一樣的最短路徑,假設(6,3)是這幾個方格中最後選擇的,則最終的結果:
從終點開始向前遍歷,能夠發現A*算法找到的最短路徑是S→(4,3) →(5,3) →(6,3) →(6,4) →(6,5) →(6,6) →E。
能夠看出,A*搜索和廣度優先搜索十分相似,兩者的候選集相同,它們的主要區別在於,廣度優先搜索的選擇是盲目的,而A*搜索是優先選擇出代價最小的那個,利用啓發的方式,使得每一步都更接近於最優解。
地圖上的每個方格都是一個節點,咱們將節點信息映射爲Node類:
class Node: def __init__(self, x, y, parent, g=0, h=0): self.x = x # 節點的行號 self.y = y # 節點的列號 self.h = h self.g = g self.f = g + h self.parent = parent # 父節點 def get_G(self): ''' 當前節點到起點的代價 :param parent: :return: ''' if self.g != 0: return self.g elif self.parent is None: self.g = 0 # 當前節點在parent的垂直或水平方向 elif self.parent.x == self.x or self.parent.y == self.y: self.g = self.parent.get_G() + 10 # 當前節點在parent的斜對角 else: self.g = self.parent.get_G() + 14 return self.g def get_H(self, end): ''' 節點到終點的距離估值 :param end: 終點座標(x,y) :return: ''' if self.h == 0: self.h = self.manhattan(self.x, self.y, end[0], end[1]) * 10 return self.h def get_F(self, end): ''' 節點的評估值 :param: end 終點座標 :return: ''' if self.f == 0: self.f = self.get_G() + self.get_H(end) return self.f def manhattan(self, from_x, from_y, to_x, to_y): ''' 曼哈頓距離 ''' return abs(to_x - from_x) + abs(to_y - from_y)
每一個節點都可以計算出本身的G值、H值和F值。在get_G()中,計算G值須要使用parent.get_G(),這是一種遞歸調用,爲了不遞歸的無用功,若是當前節點的G值已經計算過了,get_G()將直接返回結果。
接下來能夠編寫坦克尋徑的代碼,先來看一些基礎結構:
class Tank_way: ''' 使用A*搜索找到坦克的最短移動路徑 ''' def __init__(self, start, end, map2d, obstruction=1): ''' :param start: 起點座標(x,y) :param end: 終點座標(x,y) :param map: 地圖 :param obstruction: 障礙物標記 ''' self.start_x, self.start_y = start self.end = end self.map2d = map2d self.openlist = {} self.closelist = {} # 垂直和水平方向的差向量 self.v_hv = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 斜對角的差向量 self.v_diagonal = [(-1, 1), (1, 1), (1, -1), (-1, -1)] self.obstruction = obstruction # 障礙物標記 self.x_edge, self.y_edge = len(map2d), len(map2d[0]) # 地圖邊界 self.answer = None def is_in_map(self, x, y): ''' (x, y)是否中地圖內 ''' return 0 <= x < self.x_edge and 0 <= y < self.y_edge def in_closelist(self, x, y): ''' (x, y) 方格是否在closeList中 ''' return self.closelist.get((x, y)) is not None def upd_openlist(self, node): ''' 用node 替換 openlist中的對應數據 ''' self.openlist[(node.x, node.y)] = node def add_in_openlist(self, node): ''' 將node添加到 openlist ''' self.openlist[(node.x, node.y)] = node def add_in_closelist(self, node): ''' 將node添加到 closelist ''' self.closelist[(node.x, node.y)] = node def pop_min_F(self): ''' 彈出openlist中F值最小的節點 ''' key_min, node_min = None, None for key, node in self.openlist.items(): if node_min is None: key_min, node_min = key, node elif node.get_F(self.end) < node_min.get_F(self.end): key_min, node_min = key, node # 將node_min從openlist中移除 if key_min is not None: self.openlist.pop(key_min) return node_min
咱們使用二維列列表存儲地圖上的每個方格,用1表示障礙物,0表示可走的道路。openList和closeList使用字典代替列表,key是方格的座標,value是表方格的節點,這將比列表更便於執行中A*搜索中的相關操做。
注意到這裏並無像5.5.1那樣用一個列表存儲八個方向的差向量,而是將斜對角的向量拆分出來,這樣作的目的是便於應對遊戲規則中「沒法貼着障礙物移動到斜對角的方格」這一規則。假設某個方格的座標是(x,y),如今想要移動到左上方的(x’,y’)。可以移動的前提是(x,y)附近的兩個方格都不是障礙物,能夠用(x,y’)和(x’,y)來定位它們:
這種方法的好處是,只要知道(x,y)和(x’,y’),就能夠判斷是否存在阻擋移動的障礙物,而無需關心(x’,y’)具體在什麼方向:
根據這種思路編寫用於尋找待探索節點的方法:
def get_Q(self, P): ''' 找到P周圍能夠探索的節點 ''' Q = {} # 將水平或垂直方向的相應方格加入到Q for dir in self.v_hv: x, y = P.x + dir[0], P.y + dir[1] # 若是(x,y)不是障礙物而且不在closelist中,將(x,y)加入到Q if self.is_in_map(x, y) \ and self.map2d[x][y] != self.obstruction \ and not self.in_closelist(x, y): Q[(x, y)] = Node(x, y, P) # 將斜對角的相應方格加入到Q for dir in self.v_diagonal: x, y = P.x + dir[0], P.y + dir[1] # 若是(x,y)不是障礙物,且(x,y)可以與P聯通,且(x,y)不在closelist中,將(x,y)加入到Q if self.is_in_map(x, y) \ and self.map2d[x][y] != self.obstruction \ and self.map2d[x][P.y] != self.obstruction \ and self.map2d[P.x][y] != self.obstruction \ and not self.in_closelist(x, y): Q[(x, y)] = Node(x, y, P) return Q
A*搜索的完整代碼:
class Node: def __init__(self, x, y, parent, g=0, h=0): self.x = x # 節點的行號 self.y = y # 節點的列號 self.h = h self.g = g self.f = g + h self.parent = parent # 父節點 def get_G(self): ''' 當前節點到起點的代價 :param parent: :return: ''' if self.g != 0: return self.g elif self.parent is None: self.g = 0 # 當前節點在parent的垂直或水平方向 elif self.parent.x == self.x or self.parent.y == self.y: self.g = self.parent.get_G() + 10 # 當前節點在parent的斜對角 else: self.g = self.parent.get_G() + 14 return self.g def get_H(self, end): ''' 節點到終點的距離估值 :param end: 終點座標(x,y) :return: ''' if self.h == 0: self.h = self.manhattan(self.x, self.y, end[0], end[1]) * 10 return self.h def get_F(self, end): ''' 節點的評估值 :param: end 終點座標 :return: ''' if self.f == 0: self.f = self.get_G() + self.get_H(end) return self.f def manhattan(self, from_x, from_y, to_x, to_y): ''' 曼哈頓距離 ''' return abs(to_x - from_x) + abs(to_y - from_y) class Tank_way: ''' 使用A*搜索找到坦克的最短移動路徑 ''' def __init__(self, start, end, map2d, obstruction=1): ''' :param start: 起點座標(x,y) :param end: 終點座標(x,y) :param map: 地圖 :param obstruction: 障礙物標記 ''' self.start_x, self.start_y = start self.end = end self.map2d = map2d self.openlist = {} self.closelist = {} # 垂直和水平方向的差向量 self.v_hv = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 斜對角的差向量 self.v_diagonal = [(-1, 1), (1, 1), (1, -1), (-1, -1)] self.obstruction = obstruction # 障礙物標記 self.x_edge, self.y_edge = len(map2d), len(map2d[0]) # 地圖邊界 self.answer = None def is_in_map(self, x, y): ''' (x, y)是否中地圖內 ''' return 0 <= x < self.x_edge and 0 <= y < self.y_edge def in_closelist(self, x, y): ''' (x, y) 方格是否在closeList中 ''' return self.closelist.get((x, y)) is not None def upd_openlist(self, node): ''' 用node 替換 openlist中的對應數據 ''' self.openlist[(node.x, node.y)] = node def add_in_openlist(self, node): ''' 將node添加到 openlist ''' self.openlist[(node.x, node.y)] = node def add_in_closelist(self, node): ''' 將node添加到 closelist ''' self.closelist[(node.x, node.y)] = node def pop_min_F(self): ''' 彈出openlist中F值最小的節點 ''' key_min, node_min = None, None for key, node in self.openlist.items(): if node_min is None: key_min, node_min = key, node elif node.get_F(self.end) < node_min.get_F(self.end): key_min, node_min = key, node # 將node_min從openlist中移除 if key_min is not None: self.openlist.pop(key_min) return node_min def get_Q(self, P): ''' 找到P周圍能夠探索的節點 ''' Q = {} # 將水平或垂直方向的相應方格加入到Q for dir in self.v_hv: x, y = P.x + dir[0], P.y + dir[1] # 若是(x,y)不是障礙物而且不在closelist中,將(x,y)加入到Q if self.is_in_map(x, y) \ and self.map2d[x][y] != self.obstruction \ and not self.in_closelist(x, y): Q[(x, y)] = Node(x, y, P) # 將斜對角的相應方格加入到Q for dir in self.v_diagonal: x, y = P.x + dir[0], P.y + dir[1] # 若是(x,y)不是障礙物,且(x,y)可以與P聯通,且(x,y)不在closelist中,將(x,y)加入到Q if self.is_in_map(x, y) \ and self.map2d[x][y] != self.obstruction \ and self.map2d[x][P.y] != self.obstruction \ and self.map2d[P.x][y] != self.obstruction \ and not self.in_closelist(x, y): Q[(x, y)] = Node(x, y, P) return Q def a_search(self): while True: # 找到openlist中F值最小的節點做爲探索節點 P = self.pop_min_F() # openlist爲空,表示沒有通向終點的路 if P is None: break # P加入closelist self.add_in_closelist(P) # P周圍待探索的節點 Q = self.get_Q(P) # Q中沒有任何節點,表示該路徑必定不是最短路徑,從新從openlist中選擇 if Q == {}: continue # 找到了終點, 退出循環 if Q.get(self.end) is not None: self.answer = Node(self.end[0], self.end[1], P) break # Q中的節點與openlist中的比較 for item in Q.items(): (x, y), node_Q = item[0], item[1] node_openlist = self.openlist.get((x, y)) # 若是node_Q不在openlist中,直接將其加入openlist if node_openlist is None: self.add_in_openlist(node_Q) # node_Q的F值比node_openlist更小,則用node_Q替換node_openlist elif node_Q.get_F(self.end) < node_openlist.get_F(self.end): self.upd_openlist(node_Q) def start(self): node_start = Node(self.start_x, self.start_y, None) self.openlist[(self.start_x, self.start_y)] = node_start self.a_search() def paint(self): ''' 打印最短路線 ''' node = self.answer while node is not None: print((node.x, node.y), 'G={0}, H={1}, F={2}'.format(node.g, node.h, node.get_F(self.end))) node = node.parent if __name__ == '__main__': map2d = [[0] * 8 for i in range(8)] map2d[5][4] = 1 map2d[5][5] = 1 map2d[4][5] = 1 map2d[3][5] = 1 map2d[2][5] = 1 start, end = (3, 2), (5, 7) a_way = Tank_way(start, end, map2d) a_way.start() a_way.paint()
運行結果:
坦克尋徑的故事並無結束,還能夠額外考慮遊戲中的兩種典型的狀況。一種是咱們以前定義的「沒法貼着障礙物移動到斜對角的方格」並不那麼準確,若是障礙物只是佔據了單元格的一部分位置,坦克也許能夠擠過去:
另外一個狀況在遊戲中更爲常見,坦克實際上是能夠穿過樹林的,只不過在樹林中行進遠遠慢於在大路上行進。這相似於電視中的橋段:大路遠但好走,小路近而難行,至於最終哪一個更省力,全靠運氣——也許小路因爲剛下過一場雨致使更加難走,克服困難的成本遠大於原計劃節省的成本。爲了應對這種狀況,能夠爲每一個方格添加一個代價因子,一個方格的代價因子越高,移動到這裏的代價越大。例如某一點(x,y)的G值是G(x,y)=100,向垂直和水平方向的相鄰方格移動一步的代價是10;左側方格(x1,y1)是樹林,移動因子是2;右側方格(x2,y2)是平地,移動因子是1,此時:
做者:我是8位的