1. 程式人生 > >A*搜索詳解(2)——再戰覲天寶匣

A*搜索詳解(2)——再戰覲天寶匣

turn 廣度 彈出 運行 存在 range 假設 起點 ret

  書接上文。在坦克尋徑的,tank_way中,A*算法每一步搜索都是選擇F值最小的節點,步步為營,使得尋徑的結果是最優解。在這個過程中,查找最小F值的算法復雜度是O(n),這對於小地圖沒什麽問題,但是對於大地圖來說,openlist將會保存大量的節點信息,此時如果每次循環仍然使用O(n)復雜度的算法去查找最小F值就是個非常嚴重的問題了,這將導致遊戲運行緩慢。可以針對這一點行改進,在常數時間內查找到最小F值的節點。

  一個現成的數據結構是優先隊列,python的heapq模塊已經實現了這個功能,它是基於堆優先隊列,可以中O(1)時間內返回堆中的最小值。我們用heapq存儲openlist中的節點,構建新的坦克尋徑代碼:

import heapq

START, END = (), () # 起點和終點的位置
OBSTRUCTION = 1 # 障礙物標記

class Node:
    def __init__(self, x, y, parent):
        self.x = x  # 節點的行號
        self.y = y  # 節點的列號
        self.parent = parent  # 父節點
        self.h = 0
        self.g = 0
        self.f = 0

    def get_G(self):
        
‘‘‘ 當前節點到起點的代價 ‘‘‘ 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): ‘‘‘節點到終點的距離估值 ‘‘‘ if self.h == 0: self.h = self.manhattan(self.x, self.y, END[0], END[1]) * 10 return self.h def get_F(self): ‘‘‘ 節點的評估值 ‘‘‘ if self.f == 0: self.f = self.get_G() + self.get_H() 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) def __lt__(self, other): ‘‘‘ 用於堆比較,返回堆中f最小的一個 ‘‘‘ return self.get_F() < other.get_F() def __eq__(self, other): ‘‘‘ 判斷Node是否相等 ‘‘‘ return self.x == other.x and self.y == other.y def __ne__(self, other): ‘‘‘ 判斷Node是否不等 ‘‘‘ return not self.__eq__(other) class Tank_way: ‘‘‘ 使用A*搜索找到坦克的最短移動路徑 ‘‘‘ def __init__(self, map2d): self.map2d = map2d # 地圖數據 self.x_edge, self.y_edge = len(map2d), len(map2d[0]) # 地圖邊界 # 垂直和水平方向的差向量 self.v_hv = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 斜對角的差向量 self.v_diagonal = [(-1, 1), (1, 1), (1, -1), (-1, -1)] self.openlist = [] # openlist使用基於堆的優先隊列 self.closelist = set() 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 (x, y) in self.closelist def add_in_openlist(self, node): ‘‘‘ 將node添加到 openlist ‘‘‘ heapq.heappush(self.openlist, node) def add_in_closelist(self, node): ‘‘‘ 將node添加到 closelist ‘‘‘ self.closelist.add((node.x, node.y)) def pop_min_F(self): ‘‘‘ 彈出openlist中F值最小的節點 ‘‘‘ return heapq.heappop(self.openlist) def append_Q(self, P): ‘‘‘ 找到P周圍可以探索的節點,將其加入openlist,並返回這些節點 ‘‘‘ 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] != OBSTRUCTION and not self.in_closelist(x, y): node = Node(x, y, P) Q[(x, y)] = node heapq.heappush(self.openlist, node) # 將node同時放入openlist中 # 將斜對角的相應方格加入到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] != OBSTRUCTION and self.map2d[x][P.y] != OBSTRUCTION and self.map2d[P.x][y] != OBSTRUCTION and not self.in_closelist(x, y): node = Node(x, y, P) Q[(x, y)] = node heapq.heappush(self.openlist, node) # 將node同時放入openlist中 return Q def a_search(self): while self.openlist: # 找到openlist中F值最小的節點作為探索節點 P = self.pop_min_F() # 如果P在closelist中,執行下一次循環 if self.in_closelist(P.x, P.y): continue # P加入closelist self.add_in_closelist(P) # P周圍待探索的節點 Q = self.append_Q(P) # Q中沒有任何節點,表示該路徑一定不是最短路徑,重新從openlist中選擇 if not Q: continue # 找到了終點, 退出循環 if Q.get(END) is not None: self.answer = Node(END[0], END[1], P) break def start(self): node_start = Node(START[0], START[1], None) self.add_in_openlist(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())) 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(map2d) a_way.start() a_way.paint()

  Tank_way_2省略的代碼和Tank_way一致。為了讓openlist能夠返回F值最小值的節點,需要在Node中添加三個額外的方法。對於pop_min_F()而言,不再需要遍歷所有節點,僅僅是從堆頂彈出而已,這將大大縮短程序運行的時間。在Tank_way_2中,用append_Q代替了原來來的get_Q(),這是因為不再需要用Q中的節點和openlist中的節點相比較,僅僅是將Q中的節點添加到openlist中。這樣做雖然會使得openlist中存在一些重復節點,不過沒關系,對於有相同標記的節點,F值小的那個總是最先彈出,一旦彈出就會加入到closelist中,這意味著當該標記的節點再次彈出時,將不會被使用,也就是說,如果同一個標記的節點被計算了多次F值,總是能夠確保使用F值最小的那個,並丟棄其它的。

再戰覲天寶匣

  基於盲目策略的廣度優先收索無法有效完成4階以上的拼圖(可參考搜索的策略(3)——覲天寶匣上的拼圖),在理解了A*搜索後,可以用這種啟發性策略再次挑戰覲天寶匣的拼圖。

設計評估函數

  如果將拼圖的每一次移動看作“一步”,只要能定義出離評估函數和代價函數,就可以像坦克尋徑一樣使用A*搜索尋找拼圖的復原步驟。

  我們將g(n)定義為從起點移動到某個狀態的步數;h(n)是當前狀態到復原狀態的距離估值,它用所有碎片的曼哈頓距離之和表示。以3×3的拼圖為例,假設拼圖的某個狀態和復原狀態是:

技術分享圖片

  左圖中,3號碎片的位置是(2,0),它在復原狀態的位置是(1,0),則3號碎片的曼哈頓距離是|2-1|+|0-0|=1。同理,5號碎片的曼哈頓距離是|0-1|+|1-2|=2。左圖距復原狀態的曼哈頓距離是所有碎片的曼哈頓距離之和:

技術分享圖片

  其中Dn表示第n個碎片的曼哈頓距離,圖眼的編號是8。

復原拼圖

  有了g和h就可以開始復原拼圖,復原過程和坦克的尋路類似。從拼圖的初始狀態開始,第一步可以向三個方向探測,從而產生三種狀態:

技術分享圖片

  此後每一步都選擇最小的F值繼續探索,如果F值相同,則選擇最後加入openlist中的一個:

技術分享圖片

  最終的復原步驟如圖:

技術分享圖片

  對比搜索的策略(3)——覲天寶匣上的拼圖中的廣度優先搜索可以看出,A*搜索比廣度優先搜索的復原更快。

實現A*搜索

  拼圖的實現和坦克尋徑類似,完整代碼如下:

import random
import copy
import heapq

IMG_END = []  # 拼圖的復原狀態
EYE_VAL =   # 圖眼的值
DIST = {}

def get_hash_value(img):
    ‘‘‘ 獲取img的哈希值 ‘‘‘
    return hash(str(img))

class Node:
    def __init__(self, img, x=0, y=0, parent=None):
        self.img = img # 當前拼圖
        self.x, self.y = x, y # 圖眼在img中的位置
        self.parent = parent  # 父節點
        self.hash_value = get_hash_value(img) # Node的哈希值
        self.h = 0
        self.g = 0
        self.f = 0

    def get_G(self):
        ‘‘‘ 當前節點到起點的代價 ‘‘‘
        if self.g != 0:
            return self.g
        elif self.parent is None:
            self.g = 0
        else:
            self.g = self.parent.get_G() + 1
        return self.g

    def get_H(self):
        ‘‘‘ 節點到終點的距離估值 ‘‘‘
        if self.h == 0:
            self.h = self.manhattan()
        return self.h

    def get_F(self):
        ‘‘‘ 節點的評估值 ‘‘‘
        if self.f == 0:
            self.f = self.get_G() + self.get_H()
            # self.f = self.get_H()
        return self.f

    def manhattan(self):
        ‘‘ 當前拼圖到復原狀態的距離 ‘‘‘
        d = DIST.get(self.hash_value)
        if d is not None:
            return d

        dist = 0
        x_end, y_end = 0, 0  # img_end 中某一個碎片的位置
        n = len(self.img)
        for x, row in enumerate(self.img):
            for y, piece in enumerate(row):
                if piece == IMG_END[x][y]:
                    continue
                # 計算piece碎片在img_end中的位置
                if piece == EYE_VAL:
                    x_end = n - 1
                    y_end = n - 1
                else:
                    x_end = piece // n
                    y_end = piece - n * x_end
                dist += abs(x - x_end) + abs(y - y_end)

        DIST[self.hash_value] = dist
        return dist

    def __lt__(self, other):
        ‘‘‘ 用於堆比較,返回堆中f最小的一個 ‘‘‘
        return self.get_F() < other.get_F()

    def __eq__(self, other):
        ‘‘‘ 判斷Node是否相等 ‘‘‘
        return self.img.hash_value == other.img.hash_value

    def __ne__(self, other):
        ‘‘‘ 判斷Node是否不等 ‘‘‘
        return not self.__eq__(other)

    def __hash__(self):
        return self.hash_value

class JigsawPuzzle_A:
    ‘‘‘ 用A*搜索復原拼圖 ‘‘‘
    def __init__(self, level=1, img_start=None):
        self.level = level # 難度系數
        self.n = len(IMG_END)  # 拼圖的維度
        self.end_hash_value = get_hash_value(IMG_END) # 復原狀態的哈希值
        # “圖眼”移動的方向, 上、左、下、右
        self.v_move = [(0, 1), (-1, 0), (0, -1), (1, 0)]
        # 設置拼圖的初始狀態和圖眼的位置
        if img_start is not None:
            self.img_start = img_start
            self.eye_x, self.eye_y = self.search_eye(img_start)
        else:
            self.img_start, self.eye_x, self.eye_y = self.confuse()
        self.openlist = []
        self.closelist = set()
        # 拼圖復原步驟
        self.answer = None

    def confuse(self):
        ‘‘‘ 創建一個n*n的拼圖,返回打亂狀態和圖眼位置 ‘‘‘
        # 拼圖的初始狀態
        img_start = copy.deepcopy(IMG_END)
        from_x, from_y = self.search_eye(IMG_END)
        to_x, to_y = from_x, from_y
        # 將圖眼隨機移動 n * n * level次
        for i in range(self.n * self.n * self.level):
            # 選擇一個隨機方向
            v_x, v_y = random.choice(self.v_move)
            to_x, to_y = from_x + v_x, from_y + v_y
            if self.enable(to_x, to_y):
                # 向選擇的隨機方向移動
                self.move(img_start, from_x, from_y, to_x, to_y)
                from_x, from_y = to_x, to_y
            else:
                to_x, to_y = from_x, from_y

        return img_start, to_x, to_y

    def search_eye(self, img):
        ‘‘‘ 找到img中圖眼的位置 ‘‘‘
        # “圖眼”的值是eye_val,打亂順序後需要尋找到圖眼的位置
        for x in range(self.n):
            for y in range(self.n):
                if EYE_VAL == img[x][y]:
                    return  x, y

    def in_closelist(self, node):
        ‘‘‘ node 是否在closelist中 ‘‘‘
        return node.hash_value in self.closelist

    def add_in_openlist(self, node):
        ‘‘‘ node節點加入openlist ‘‘‘
        heapq.heappush(self.openlist, node)

    def add_in_closelist(self, node):
        ‘‘‘ node節點加入closelist ‘‘‘
        self.closelist.add(node.hash_value)

    def pop_min_F(self):
        ‘‘‘ 找到openlist中F值最小的節點 ‘‘‘
        return heapq.heappop(self.openlist)

    def enable(self, to_x, to_y):
        ‘‘‘ 圖眼是否能夠移動到x,y的位置 ‘‘‘
        return 0 <= to_x < self.n and 0 <= to_y < self.n

    def move(self, img, from_x, from_y, to_x, to_y):
        ‘‘‘ 將圖眼從from_x, from_y移動到to_x, to_y ‘‘‘
        img[from_x][from_y], img[to_x][to_y] = img[to_x][to_y], img[from_x][from_y]

    def append_Q(self, P):
        ‘‘‘ 找到P周圍可以探索的節點,將其加入openlist,並返回這些節點 ‘‘‘
        Q = {}
        for v_x, v_y in self.v_move:
            to_x, to_y = P.x + v_x, P.y + v_y
            # 檢驗是否可以向to_x, to_y方向移動
            if not self.enable(to_x, to_y):
                continue

            curr_img = copy.deepcopy(P.img)
            self.move(curr_img, P.x, P.y, to_x, to_y)
            # 如果node是不在closelist中,把node添加到Q中
            if not self.in_closelist(Node(curr_img)):
                node = Node(curr_img, x=to_x, y=to_y, parent=P)
                Q[node.hash_value] = node
                self.add_in_openlist(node)
        return Q

    def a_search(self):
        ‘‘‘ A*搜索拼圖的解 ‘‘‘
        while self.openlist:
            # 找到openlist中F值最小的節點作為探索節點
            P = self.pop_min_F()
            # 如果P在closelist中,執行下一次循環
            if self.in_closelist(P):
                continue
            # P加入closelist
            self.add_in_closelist(P)
            # P周圍待探索的節點
            Q = self.append_Q(P)
            # Q中沒有任何節點,表示該路徑一定不是最短路徑,重新從openlist中選擇
            if not Q:
                continue
            # 找到了終點, 退出循環
            if Q.get(self.end_hash_value) is not None:
                self.answer = Node(IMG_END, parent=P)
                break

    def start(self):
        if self.img_start == IMG_END:
            print(start = end)
            return
        node_start = Node(img=self.img_start, x=self.eye_x, y=self.eye_y)
        self.add_in_openlist(node_start)
        self.a_search()

    def display(self):
        if self.answer is None:
            print(No answer)

        node = self.answer
        while node is not None:
            print(node.img)
            node = node.parent

def create_img_end(n):
    ‘‘‘ 創建一個n*n的拼圖,將右下角的碎片圖指定為圖眼 ‘‘‘
    img = []
    for i in range(n):
        img.append(list(range(n * i, n * i + n)))
    img[n - 1][n - 1] = EYE_VAL
    return img

if __name__ == __main__:
    n = 9
    IMG_END = create_img_end(n)
    # img_start = [[3, 0, 2], [1, 7, EYE_VAL], [6, 5, 4]]
    jigsaw = JigsawPuzzle_A(level=5)
    print(start=, jigsaw.img_start, ,eye =, (jigsaw.eye_y, jigsaw.eye_x))
    jigsaw.start()
    jigsaw.display()

  JigsawPuzzle_A中額外設置了難度系數,level的值越大,復原拼圖越困難。對於一個拼圖來說,level=5已經足以打亂順序:

技術分享圖片

  九九拼圖的復原已經非人力所能解決。JigsawPuzzle_A可以快速復原任意難度的4×4拼圖,對於更高階的拼圖,即使是A*搜索,面對的搜索數量依然十分龐大,需要耗費相當長的時間,只有level=1的時候 9×9拼圖才能快速得到結果。

  


   作者:我是8位的

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

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

  掃描二維碼關註公眾號“我是8位的”

技術分享圖片

A*搜索詳解(2)——再戰覲天寶匣