本文章適合黃金段位的LOL大神,同樣更適合出門在外沒有導航,就找不到家的孩子。
在英雄聯盟之中,當你和你的隊友都苦苦修煉到十八級的時候,仍然與敵方陣營不分勝負,就在你剛買好裝備已經神裝的時候,你看見信息框中一條隊友的消息:“大龍集合”,這個時候你鼠標移到大龍處,輕點右鍵,然后你就像一個吃瓜群眾一樣盯着你的英雄,看他走進野區小路,因為你買了日炎斗篷,路過三狼的時候三狼還追着你咬了幾口,你的英雄也沒有去理會,三狼可算是出了一口氣,牛逼壞了!然后你還順路采了幾個蘑菇,因燙到了藍buff被藍buff追殺。就連河道里的河蟹都想咬你一口為你在三級的時候殺了它的爺爺而報仇。然而你還是在臨死前來到大龍面前,你還沒動大龍一根汗毛,就被大龍一個甩尾干趴下了,這時候你旁邊的妹紙還很疑惑,你得顯示器怎么突然壞掉了,變成黑白的了。
那么問題來了,為什么野區套路那么深,而你的英雄不選擇走大路沿河道到大龍呢?因為你每確定一個目標,你的英雄就會沿着最短的路線前往。那么你的英雄是怎么找到最近的路線呢?如果你覺的很簡單,你自己也能找到,你有你的英雄找的快嗎?當你確定目標的時候你的英雄可不是東張西望讓后才開始走,更不會走一半發現不對勁有自己回去重頭再來。你也許開始對這個問題感興趣了,那些游戲中的英雄人物是怎么做到的?如果你不玩游戲,那么你肯定用過導航軟件,你應該會好奇它是怎么做到的。你能讀到這篇文章,那么你一定會寫代碼,你能用代碼去實現這個功能嗎?其實我一直都很好奇這個是怎么做到的,我最多也就會寫一些增刪改查的常規操作。直到我接到了一個實現A-star算法的作業,才弄明白。
A-star算法
我們假設某個人要從A點到達B點,而一堵牆把這兩個點隔開了,如下圖所示,綠色 部分代表起點A,紅色部分代表終點B,藍色方塊部分代表之間的牆。
你首先會注意到我們把這一塊搜索區域分成了一個一個的方格,如此這般,使搜索 區域簡單化,正是尋找路徑的第一步。這種方法將我們的搜索區域簡化成了一個普 通的二維數組。數組中的每一個元素表示對應的一個方格,該方格的狀態被標記為 可通過的和不可通過的。通過找出從A點到B點所經過的方格,就能得到AB之間的 路徑。當路徑找出來以后,這個人就可以從一個格子中央移動到另一個格子中央, 直到抵達目的地。 這些格子的中點叫做節點。當你在其他地方看到有關尋找路徑的東西時,你會經常發現人們在討論節點。為什么不直接把它們稱作方格呢?因為你不一定要把你的搜 索區域分隔成方塊,矩形、六邊形或者其他任何形狀都可以。況且節點還有可能位 於這些形狀內的任何一處呢?在中間、靠着邊,或者什么的。我們就用這種設定, 因為畢竟這是最簡單的情況。
當我們把搜索區域簡化成一些很容易操作的節點后,下一步就要構造一個搜索來尋 找最短路徑。在A*算法中,我們從A點開始,依次檢查它的相鄰節點,然后照此繼 續並向外擴展直到找到目的地。 我們通過以下方法來開始搜索:
1. 從A點開始,將A點加入一個專門存放待檢驗的方格的“開放列表”中。這個開放列表 有點像一張購物清單。當前這個列表中只有一個元素,但一會兒將會有更多。列表 中包含的方格可能會是你要途經的方格,也可能不是。總之,這是一個包含待檢驗 方格的列表。
2.檢查起點A相鄰的所有可達的或者可通過的方格,不用管牆啊,水啊,或者其他什 么無效地形,把它們也都加到開放列表中。對於每一個相鄰方格,將點A保存為它 們的“父方格”。當我們要回溯路徑的時候,父方格是一個很重要的元素。稍后我們 將詳細解釋它。
3.從開放列表中去掉方格A,並把A加入到一個“封閉列表”中。封閉列表存放的是你現 在不用再去考慮的方格。
此時你將得到如下圖所示的樣子。在這張圖中,中間深綠色的方格是你的起始方格, 所有相鄰方格目前都在開放列表中,並且以亮綠色描邊。每個相鄰方格有一個灰色 的指針指向它們的父方格,即起始方格
接下來,我們在開放列表中選一個相鄰方格並再重復幾次如前所述的過程。但是我 們該選哪一個方格呢?具有最小F值的那個
路徑排序
決定哪些方格會形成路徑的關鍵是這個等式:F = G + H
G=從起點A沿着已生成的路徑到一個給定方格的移動開銷
H=從給定方格到目的方格的估計移動開銷。這種方式常叫做試探,有點困惑人吧。 其實之所以叫做試探法是因為這只是一個猜測。在找到路徑之前我們實際上並不知 道實際的距離,因為任何東西都有可能出現在半路上(牆啊,水啊什么的)。本文中 給出了一種計算H值的方法,網上還有很多其他文章介紹的不同方法
我們要的路徑是通過反復遍歷開放列表並選擇具有最小F值的方格來生成的。本文稍 后將詳細討論這個過程。我們先進一步看看如何計算那個等式。
如前所述,G是從起點A沿着已生成的路徑到一個給定方格的移動開銷,在本例中, 我們指定每一個水平或者垂直移動的開銷為 10,對角線移動的開銷為 14。因為對角 線的實際距離是 2 的平方根(別嚇到啦),或者說水平及垂直移動開銷的 1.414 倍。 為了簡單起見我們用了 10 和 14 這兩個值。比例大概對就好,我們還因此避免了平 方根和小數的計算。這倒不是因為我們笨或者說不喜歡數學,而是因為對電腦來說, 計算這樣的數字也要快很多。不然的話你會發現尋找路徑會非常慢。
我們要沿特定路徑計算給定方格的G值,辦法就是找出該方格的父方格的G值,並根 據與父方格的相對位置(斜角或非斜角方向)來給這個G值加上 14 或者 10。在本例 中這個方法將隨着離起點方格越來越遠計算的方格越來越多而用得越來越多。
有很多方法可以用來估計H值。我們用的這個叫做曼哈頓(Manhattan)方法, 即計算通過水平和垂直方向的平移到達目的地所經過的方格數乘以 10 來得到H值。之所 以叫Manhattan方法是因為這就像計算從一個地方移動到另一個地方所經過的城市 街區數一樣,而通常你是不能斜着穿過街區的。重要的是,在計算H值時並不考慮 任何障礙物。因為這是對剩余距離的估計值而不是實際值(通常是要保證估計值不大於實際值)。這就是為什么這個方式被叫做試探法的原因了。
G和H相加就得到了F。第一步搜索所得到的結果如下圖所示。每個方格里都標出了F、 G和H值。如起點方格右側的方格標出的,左上角顯示的是F值,左下角是G值,右 下角是H值。
我們來看看這些方格吧。在有字母的方格中,G=10,這是因為它在水平方向上離 起點只有一個方格遠。起點緊挨着的上下左右都具有相同的G值 10。對角線方向的 方塊G值都是 14。
H值通過估算到紅色目標方格的曼哈頓距離而得出。用這種方法得出的起點右側方 格到紅色方格有 3 個方格遠,則該方格H值就是 30。上面那個方格有 4 個方格遠(注 意只能水平和垂直移動),H就是 40。你可以大概看看其他方格的H值是怎么計算出 來的。
每一個方格的F值,當然就不過是G和H值之和了。
繼續搜索
為了繼續搜索,我們簡單的從開放列表中選擇具有最小 F 值的方格,然后對選中的 方格進行如下操作:
4.將其從開放列表中移除,並加到封閉列表中。
5.檢驗所有的相鄰方格,忽略那些不可通過的或者已經在封閉列表里的方格。如果這 個相鄰方格不在開放列表中,就把它添加進去。並將當前選定方格設為新添方格的 父方格。
6.如果某個相鄰方格已經在開放列表中了(意味着已經探測過,而且已經設置過父方 格――譯者),就看看有沒有到達那個方格的更好的路徑。也就是說,如果從當前選 中方格到那個方格,會不會使那個方格的 G 值更小。如果不能,就不進行任何操作。
相反的,如果新路徑的 G 值更小,就將該相鄰方格的父方格重設為當前選中方格。
(在上圖中是改變其指針的方向為指向選中方格。最后,重新計算那個相鄰方格的 F 和 G 值。如果你看糊塗了,下面會有圖解說明。
好啦,咱們來看看具體點的例子。在初始時的 9 個方塊中,當開始方格被加到封閉 列表后,開放列表里還剩 8 個方格。在這八個方格當中,位於起點方格右邊的那個 方格具有最小的 F 值 40。所以我們選擇這個方格作為下一個中心方格。下圖中它以 高亮的藍色表示。
首先,我們將選中的方格從開放列表中移除,並加入到封閉列表中(所以用亮藍色 標記)。然后再檢驗它的相鄰節點。那么在它緊鄰的右邊的方格都是牆,所以不管它 們。左邊挨着的是起始方格,而起始方格已經在封閉列表中了,所以我們也不管它。
其他四個方格已經在開放列表中,那么我們就要檢驗一下如果路徑經由當前選中方 格到那些方格的話會不會更好,當然,是用 G 值作為參考。來看看選中方格右上角 的那一個方格,它當前的 G 值是 14,如果我們經由當前節點再到達那個方格的話, G 值會是 20(到當前方格的 G 值是 10,然后向上移動一格就再加上 10)。為 20 的 G 值比 14 大,因此這樣的路徑不會更好。你看看圖就會容易理解些。顯然從起始點 沿斜角方向移動到那個方格比先水平移動一格再垂直移動一格更直接。
當我們按如上過程依次檢驗開放列表中的所有四個方格后,會發現經由當前方格的 話不會形成更好的路徑,那我們就保持目前的狀況不變。現在我們已經處理了所有 相鄰方格,准備到下一個方格吧。
我們再遍歷一下開放列表,目前只有 7 個方格了。我們挑個 F 值最小的吧。有趣的 是,目前這種情況下,有兩個 F 值為 54 的方格。那我們怎么選擇呢?其實選哪個都 沒關系,要考慮到速度的話,選你最近加到開放列表中的那一個會更快些。當離目 的地越來越近的時候越偏向於選最后發現的方格。實際上這個真的沒關系(對待這 個的不同造成了兩個版本的 A*算法得到等長的不同路徑)。
那我們選下面的那個好了,就是起始方格右邊的,下圖所示的那個
這一次,在我們檢驗相鄰方格的時候發現右邊緊挨的那個是牆,就不管它了。上面 挨着的那個也同樣忽略。還有右邊牆下面那個方格我們也不管。為什么呢?因為你 不可能切穿牆角直接到達那個格子。實際上你得先向下走然后再通過那個方格。這 個過程中是繞着牆角走。(注意:穿過牆角的這個規則是可選的,取決於你的節點是 如何放置的。)
那么還剩下其他五個相鄰方格。當前方格的下面那兩個還不在開放列表中,那我們 把它們加進去並且把當前方格作為它們的父方格。其他三個中有兩個已經在封閉列 表中了(兩個已經在圖中用亮藍色標記了,起始方格,上面的方格),所以就不用管 了。最后那個,當前方格左邊挨着的,要檢查一下經由當前節點到那里會不會降低 它的 G 值。結果不行,所以我們又處理完畢了,然后去檢驗開放列表中的下一個格 子。
重復這個過程直到我們把目的方格加入到開放列表中了,那時候看起來會像下圖這個樣子。
注意到沒?起始方格下兩格的位置,那里的格子已經和前一張圖不一樣了。之前它 的 G 值是 28 並且指向右上方的那個方格。現在它的 G 值變成了 20 並且指向了正上 方的方格。這個改變是在搜索過程中,它的 G 值被核查時發現在某個新路徑下可以 變得更小時發生的。然后它的父方格也被重設並且重新計算了 G 值和 F 值。在本例 中這個改變看起來好像不是很重要,但是在很多種情況下這種改變會使到達目標的 最佳路徑變得非常不同。
那么我們怎樣來自動得出實際路徑的呢?很簡單,只要從紅色目標方格開始沿着每一 個方格的指針方向移動,依次到達它們的父方格,最終肯定會到達起始方格。那就 是你的路徑!如下圖所示。從 A 方格到 B 方格的移動就差不多是沿着這個路徑從每 個方格中心(節點)移動到另一個方格中心,直到抵達終點。
以下是一個python實現的a-start作業實例
Introduction
With a suitable abstractions planning a path for a mobile robot can be converted into a search problem. Begin by abstracting the environment into 2D grid of square “cells”. The robot state can be represented by a [x,y] pair, with [0,0] being the top-left cell of the grid. Movements of the robot are possible at any time either UP, DOWN, LEFT RIGHT (denoted U, D, L, R) unless the neighbouring cell is occupied. The UP action moves to the cell immediately above (if possible) by changing [x,y] to [x,y-1]. Likewise RIGHT changes the current state from [x,y] to [x+1,y], and so on. Given a start state, the goal of the robot is to reach a new (user specified) state [X*, Y*], where this must be an unoccupied cell in the grid. An action that would take the robot outside the grid or into and occupied cell results in no change to the current state.
Your task is to write a program that will read in an environment, a start state and a goal state, and conduct a search to find a path between start and goal. You may implement any of the search algorithms discussed in lectures. Your output should be in the form of a space-separated list of actions (e.g. U R R D D L L).
Test data are provided on the course pages along with this Assignment description. A week before the deadline more data will be made available and you must run your code on these new data and include these results in your report (see below).
You must write the program yourself in either C, C++, Java or Python. If you use a library package or language function call for doing the search, you will be limited to 50% of the available marks (noting that this assignment is a hurdle for the course with min mark to achieve of hurdle of 45%). If there is evidence you have simply copied code from the web, you will be awarded no marks and referred for plagiarism.
The program must accept 5 arguments from the command-line, a filename (which contains the environment) and 4 integers specifying start state and goal state, eg:
./robotplanner env.txt 0 2 4 1
would read the environment specification from the file env.txt and should plan a path from [0 2] to goal state [4 1]
Submission
You must submit, by the due date, a zip file containing:
1. your code
2. a document (briefly) describing your implementation and detailing your results
I will update as soon as possible whether there will be web-testing and auto-marking for your program outputs. This assignment is due 11.59pm on Thursday 5th April, 2018. If your submission is late, the maximum mark you can obtain will be reduced by 25% per day (or part thereof) past the due date or any extension you are granted.
Optional component
The search algorithm you use is deliberately not specified, however extra marks will be available for a successful implementation and description of A* search. It is up to you how you define the heuristic.
File format
The environment will be stored as text file in the following format: the first line contains the width and height as two (space or tab separated) integers. Each subsequent line contains a set of space-or-tab-separated 0s and 1s, with 0 representing that a cell is freespace (and therefore navigable) and 1 indicating it is occupied, and therefore cannot be entered or passed through by the robot. For example
5 3 0 0 1 0 0 1 0 1 0 0 0 0 0 0 1
If we suppose that the start state is [0,0] (i.e. at the top left) and the goal state is [4,0] (i.e. top right), then a valid solutions is
R D D R R U U R
Note that this solution is optimal but not unique.
Assessment
Assessment will be made of the basis of quality of code (40), quality of write-up (30) and accuracy of results (30) for a total of 100. Up to 25 bonus marks are available from successful completion of an A* implementation, but the maximum mark remains 100.
Prof. Ian Reid, 10 March 2018
代碼實現python3
astar.py
import sys #地圖(從文件中獲取的二維數組) maze=[] #起點 start=None #終點 end=None #開放列表(也就是有待探查的地點) open_list={} #關閉列表 (已經探查過的地點和不可行走的地點) close_list={} #地圖邊界(二維數組的大小,用於判斷一個節點的相鄰節點是否超出范圍) map_border=() #方向 orientation=[] class Node(object): def __init__(self,father,x,y): if x<0 or x>=map_border[0] or y<0 or y>=map_border[1]: raise Exception('坐標錯誤') self.father=father self.x=x self.y=y if father !=None: self.G=father.G+1 self.H=distance(self,end) self.F=self.G+self.H else: self.G=0 self.H=0 self.F=0 def reset_father(self,father,new_G): if father!=None: self.G=new_G self.F=self.G+self.H self.father=father #計算距離 def distance(cur,end): return abs(cur.x-end.x)+abs(cur.y-end.y) #在open_list中找到最小F值的節點 def min_F_node(): global open_list if len(open_list)==0: raise Exception('路徑不存在') _min=9999999999999999 _k=(start.x,start.y) #以列表的形式遍歷open_list字典 for k,v in open_list.items(): if _min>v.F: _min=v.F _k=k return open_list[_k] #把相鄰的節點加入到open_list之中,如果發現終點說明找到終點 def addAdjacentIntoOpen(node): global open_list,close_list #首先將該節點從開放列表移動到關閉列表之中 open_list.pop((node.x,node.y)) close_list[(node.x,node.y)]=node adjacent=[] #添加相鄰節點的時候要注意邊界 #上 try: adjacent.append(Node(node,node.x,node.y-1)) except Exception as err: pass #下 try: adjacent.append(Node(node,node.x,node.y+1)) except Exception as err: pass #左 try: adjacent.append(Node(node,node.x-1,node.y)) except Exception as err: pass #右 try: adjacent.append(Node(node,node.x+1,node.y)) except Exception as err: pass #檢查每一個相鄰的點 for a in adjacent: #如果是終點,結束 if (a.x,a.y)==(end.x,end.y): new_G=node.G+1 end.reset_father(node,new_G) return True #如果在close_list中,不去理他 if (a.x,a.y) in close_list: continue #如果不在open_list中,則添加進去 if (a.x,a.y) not in open_list: open_list[(a.x,a.y)]=a #如果存在在open_list中,通過G值判斷這個點是否更近 else: exist_node=open_list[(a.x,a.y)] new_G=node.G+1 if new_G<exist_node.G: exist_node.reset_father(node,new_G) return False #查找路線 def find_the_path(start,end): global open_list open_list[(start.x,start.y)]=start the_node=start try: while not addAdjacentIntoOpen(the_node): the_node=min_F_node() except Exception as err: #路徑找不到 print(err) return False return True #讀取文件,將文件中的信息加載到地圖(maze)信息中 def readfile(url): global maze,map_border f=open(url) line=f.readline() map_size=line.split() map_size=list(map(int,map_size)) x=map_size[0] y=map_size[1] map_border=(x,y) i=0 while line: line=f.readline() maze.append(list(map(int,line.split()))) i=i+1 if i>x-1: break #通過遞歸的方式根據每個點的父節點將路徑連起來 def mark_path(node): global orientation if node.father==None: return #print('({x},{y})'.format(x=node.x,y=node.y)) #將方向信息存儲到方向列表中 if node.father.x-node.x>0: orientation.append('L') elif node.father.x-node.x<0: orientation.append('R') elif node.father.y-node.y>0: orientation.append('U') elif node.father.y-node.y<0: orientation.append('D') mark_path(node.father) #解析地圖,把不可走的點直接放到close_list中 def preset_map(): global start,end,map_bloder,maze row_index=0 for row in maze: col_index=0 for n in row: if n==1: block_node=Node(None,col_index,row_index) close_list[(block_node.x,block_node.y)]=block_node col_index=col_index+1 row_index=row_index+1 if __name__=='__main__': #判斷在控制台輸入的參數時候達到要求 if len(sys.argv)<6: raise Exception('參數格式:文件名 x1 y1 x2 y2 其中x1 y1代表開始坐標,x2 y2代表目標坐標') else: #從控制台讀取參數 readfile(sys.argv[1]) start_x=int(sys.argv[2]) start_y=int(sys.argv[3]) end_x=int(sys.argv[4]) end_y=int(sys.argv[5]) start=Node(None,start_x,start_y) end=Node(None,end_x,end_y) preset_map() #判斷起點終點是否符合要求 if (start.x,start.y) in close_list or (end.x,end.y) in close_list: raise Exception('輸入的坐標不可走') if find_the_path(start,end): mark_path(end) #列表方向調整為起點開始 orientation.reverse() str_ori='' for o in orientation: str_ori=str_ori+o+' ' print(str_ori)
測試文件


20 15 1 1 0 0 1 0 0 0 0 0 1 1 1 0 0 1 0 1 1 1 0 0 1 0 1 1 1 0 0 1 0 0 1 1 0 1 0 0 0 0 1 0 0 0 1 0 1 1 0 0 0 0 0 1 0 0 1 1 1 0 0 0 0 0 1 0 1 1 0 0 0 0 1 1 0 1 1 0 0 1 0 0 0 1 0 0 0 1 1 0 1 0 1 1 0 1 0 1 0 0 0 1 1 1 1 1 0 1 1 1 1 0 1 0 0 1 0 1 0 1 0 1 1 1 1 1 0 0 0 0 0 0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 1 1 1 1 0 0 0 0 1 1 1 0 1 1 1 1 0 1 1 0 0 1 1 1 1 0 0 0 0 1 1 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0 1 0 0 1 0 1 0 1 1 0 0 0 0 1 1 0 1 0 1 0


5 3 0 0 1 0 0 1 0 1 0 0 0 0 0 0 1
運行結果