A*搜索詳解(1)——通往基地的最短路線

  假設地圖上有一片樹林,坦克須要繞過樹林,走到另外一側的軍事基地,在無數條行進路線中,哪條纔是最短的?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*搜索的步驟

  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*搜索

  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位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途! 

  掃描二維碼關注公衆號「我是8位的」

相關文章
相關標籤/搜索