03-0002 Python實現A*算法

1.算法描述

A*搜尋算法俗稱A星算法,是比較流行的啓發式搜索算法之一,被廣泛應用於路徑優化領域。它的獨特之處是檢查最短路徑中每個可能的節點時引入了全局信息,對當前節點距終點的距離做出估計,並作爲評價該節點處於最短路線上的可能性的量度。

A*改變它自己行爲的能力基於啓發式代價函數,啓發式函數在遊戲中非常有用。在速度和精確度之間取得折衷將會讓你的遊戲運行得更快。在很多遊戲中,你並不真正需要得到最好的路徑,僅需要近似的就足夠了。而你需要什麼則取決於遊戲中發生着什麼,或者運行遊戲的機器有多快。

在本實驗中,爲了解決羅馬尼亞度假問題而引入的A算法,目的是爲了尋找一個城市到另一個城市的最短路徑,A算法

擴展:本來想些啓發式算法的,但是突然迷茫了一下,可以以後再寫,好好的學一學。當我寫這些東西的時候,我發現,自己還不是很懂。
以上多數文字直接來源於度娘百科,沒有過多的修改。我對於A*算法的描述,着實少之又少,感覺自己又回到了最初的懵懂狀態。
我需要一個比較好的數據集來解釋自己想要時候的,直接用數據來描述自己想要表達的。

2.問題描述

旅行商問題:
In the following map with 20 cities, please find a route from Zerind to Bucharest using A search.*
[在一個擁有20個城市的圖中,使用A星算法,找到一條最優路徑從塞爾維亞到羅馬尼亞]
在這裏插入圖片描述
解釋:

上述圖中,線上的數字表示兩個城市的實際距離。
右側的數字表示到Bucharest[終點]的估計距離。
每個城市的首字母都不一樣,可以數字化。

3.算法原理

A*算法是一種搜索算法,在算法過程中引入了F,G,H三個量。
F=G+HF=G+H

F使[]FF:用作判斷條件的量。在算法使用的數據結構[優先級隊列]中,F小的將會先彈出。
GG:從出發點到當前節點的實際距離。
HH:從當前節點到目標節點的估計距離。

下面引入具體數據,來演示A*算法:
.在這裏插入圖片描述
說明:

pS12PF12Sp_{S}^{12}:P節點的F值是12,父節點是S
PQPQ:優先級隊列
SKSK:棧

注意事項:

1.PQPQ1.每次進入PQ的結點是上一次從PQ彈出結點的臨近結點。
2.PQSK2.從PQ中彈出的結點放入SK中
3.PQF3.進入PQ的結點若已經存在,比較F值,小的留下
4.SKPQ4.已經存在於SK中的結點不進入PQ中
5.5.到達目標結點就終止運算

過程:

PQSnone12PQ:S_{none}^{12}
SKnoneSK:none
.
PQSnone12ds12es13ps12PQ:\bcancel{S_{none}^{12}},d_{s}^{12},e_{s}^{13},p_{s}^{12}
SKSSK:S
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13ps12bd15cd16ed9\xcancel{e_{s}^{13}},p_{s}^{12},b_{d}^{15},c_{d}^{16},e_{d}^{9} [e][這裏需要替換e結點]
SKSdSK:S,d
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13ps12bd15cd16\xcancel{e_{s}^{13}},p_{s}^{12},b_{d}^{15},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12h_{e}^{12}, re20r_{e}^{20} [ph][此時彈出p,h皆可]
SKSdeSK:S,d,e
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16b_{d}^{15},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12h_{e}^{12}, re20r_{e}^{20}qp25q_{p}^{25}
SKSdepSK:S,d,e,p
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16b_{d}^{15},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20r_{e}^{20}qp25\xcancel{q_{p}^{25}}qh19q_{h}^{19} [pSKqPQpq][此時p在SK,q在PQ,p不進,q換掉]
SKSdephSK:S,d,e,p,h
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16\bcancel{b_{d}^{15}},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20r_{e}^{20}qp25\xcancel{q_{p}^{25}}qh19q_{h}^{19}ab14a_{b}^{14}
SKSdephbSK:S,d,e,p,h,b
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16\bcancel{b_{d}^{15}},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20r_{e}^{20}qp25\xcancel{q_{p}^{25}}qh19q_{h}^{19}ab14\bcancel{a_{b}^{14}} [a][a沒有臨近結點,彈出下一個]
SKSdephbaSK:S,d,e,p,h,b,a
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16\bcancel{b_{d}^{15}},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20\xcancel{r_{e}^{20}}qp25\xcancel{q_{p}^{25}}qh19\bcancel{q_{h}^{19}}ab14\bcancel{a_{b}^{14}}rq19r_{q}^{19}
SKSdephbaqSK:S,d,e,p,h,b,a,q
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16\bcancel{b_{d}^{15}},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20\xcancel{r_{e}^{20}}qp25\xcancel{q_{p}^{25}}qh19\bcancel{q_{h}^{19}}ab14\bcancel{a_{b}^{14}}rq19\bcancel{r_{q}^{19}}fr18f_{r}^{18}
SKSdephbaqrSK:S,d,e,p,h,b,a,q,r
.
PQSnone12PQ:\bcancel{S_{none}^{12}}, ds11\bcancel{d_{s}^{11}}, es13\xcancel{e_{s}^{13}}, ps12\bcancel{p_{s}^{12}}bd15cd16\bcancel{b_{d}^{15}},c_{d}^{16},ed9\bcancel{e_{d}^{9}}, he12\bcancel{h_{e}^{12}}, re20\xcancel{r_{e}^{20}}qp25\xcancel{q_{p}^{25}}qh19\bcancel{q_{h}^{19}}ab14\bcancel{a_{b}^{14}}rq19\bcancel{r_{q}^{19}}fr24\bcancel{f_{r}^{24}}cf25c_{f}^{25}Gf23G_{f}^{23} [G][得到了G進行終止]
SKSdephbaqrfSK:S,d,e,p,h,b,a,q,r,f
.

回溯:Gf23G_{f}^{23}fr24\bcancel{f_{r}^{24}}rq19\bcancel{r_{q}^{19}}qh19\bcancel{q_{h}^{19}}he12\bcancel{h_{e}^{12}}ed9\bcancel{e_{d}^{9}}ds11\bcancel{d_{s}^{11}}Snone12\bcancel{S_{none}^{12}}
最終結果:SdehqrfGS\rightarrow d\rightarrow e\rightarrow h\rightarrow q\rightarrow r\rightarrow f\rightarrow G

具體的實驗過程中需要用某種數據結構來保存父節點,以便回溯,而且最終G結點的F值,就是實際路程。自我感覺良好,上面的那一段我輸入了好長時間。不知道是什麼原因,過程特別的卡頓,我猜是因爲要解析好多的符號,可能需要消耗時間,比較少的時候還好,很多的話,就不如插入一張圖片。這也是瀏覽器加載的時候需要注意的,加載一張圖遠比加載好多圖來得快,所以會將好多圖標都繪製在一張圖上,在網頁中只顯示圖片中的某一部分,這樣一來,打開這樣的網頁,圖片就只加載了一次。

流程圖如下:
在這裏插入圖片描述

這是很久以前繪製的流程圖,可能不是很準確但是是根據僞代碼畫出來的,應該不會有錯。

僞代碼:
在這裏插入圖片描述

4.算法源碼

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# redefine place
A = 0  # Arad
B = 1  # Bucharest
C = 2  # Craiova
D = 3  # Dobreta
E = 4  # Eforie
F = 5  # Fagaras
G = 6  # Giurgiu
H = 7  # Hirsova
I = 8  # Iasi
L = 9  # Lugoj
M = 10  # Mehadia
N = 11  # Neamt
O = 12  # Oradea
P = 13  # Pitesti
R = 14  # Rimnicu Vilcea
S = 15  # Sibiu
T = 16  # Timisoara
U = 17  # Urziceni
V = 18  # Vaslui
Z = 19  # Zerind

# 構建一個20*20的矩陣

PLACES_NUM = 20

# 地圖
class Graph:
    def __init__(self):
        self.romania_graph = [[0] * PLACES_NUM for i in range(PLACES_NUM)]

    def add_edge(self, _from, _to, _value):
        if _from < PLACES_NUM:
            if _to < PLACES_NUM:
                self.romania_graph[_from][_to] = _value
                self.romania_graph[_to][_from] = _value

    def get_edge(self, _from, _to):
        return self.romania_graph[_from][_to]


#問題的最終解
class Stack:

    def __init__(self):
        self.stack = []

    def push(self, value):
        self.stack.append(value)
        return

    def pop(self):
        return self.stack.pop()

    def is_empty(self):
        if self.stack:
            return False
        return True

# CLOSED表
class Queue:
    def __init__(self):
        self.queue = []

    def put(self, value):
        self.queue.append(value)
        return

    def get(self):
        return self.queue.pop(0)

    def contain(self, value):
        return value in self.queue

    def is_empty(self):
        if self.queue:
            return False
        return True

# OPEN表
class PriorityQueue:

    def __init__(self):
        self.queue = []

    def put(self, node_cost):
        """
        :param node_cost: [value,cost]
        """
        self.queue.append(node_cost)

    def get(self):
        if self.queue:
            min_i = 0
            min_cost = self.queue[min_i][1]
            for i in range(len(self.queue)):
                if self.queue[i][1] < min_cost:
                    min_i = i
                    min_cost = self.queue[i][1]
            return self.queue.pop(min_i)

    def contain(self,value):
        for i in range(len(self.queue)):
            if self.queue[i][0]==value:
                return self.queue[i],True
        return None,False

    def is_empty(self):
        if self.queue:
            return False
        return True


# initialize undirected graph
# 初始化無向圖
def init_graph():
    graph = Graph()

    graph.add_edge(O, Z, 71)
    graph.add_edge(O, S, 151)
    graph.add_edge(A, Z, 75)
    graph.add_edge(A, S, 140)
    graph.add_edge(A, T, 118)
    graph.add_edge(T, L, 111)
    graph.add_edge(L, M, 70)
    graph.add_edge(M, D, 75)
    graph.add_edge(D, C, 120)
    graph.add_edge(S, R, 80)
    graph.add_edge(S, F, 99)
    graph.add_edge(R, C, 146)
    graph.add_edge(F, B, 211)
    graph.add_edge(R, P, 97)
    graph.add_edge(C, P, 138)
    graph.add_edge(P, B, 101)
    graph.add_edge(B, G, 90)
    graph.add_edge(B, U, 85)
    graph.add_edge(U, H, 98)
    graph.add_edge(U, V, 142)
    graph.add_edge(V, I, 92)
    graph.add_edge(I, N, 87)
    graph.add_edge(H, E, 86)

    return graph


def a_star(graph, h, _root, _goal):
    g = [0] * PLACES_NUM  # g(n) value                          現階段搜索過程中的每個結點的g(n)記錄@
    parents = [0] * PLACES_NUM  # temporary parents             現階段搜索過程中的每個節點的父結點記錄@
    pq_open = PriorityQueue()  # open queue                     初始化open表@
    q_close = Queue()  # closed queue                           初始化closed表@
    s_path = Stack()  # solution nodes path                     s_path中作爲走過路徑記錄@
    s_parent = Stack()  # solution nodes' parents path          s_parent中作爲open表每次pop的記錄@
    pq_open.put([_root, 0])                                     #將初始結點存入OPEN表中@
    
    while pq_open.is_empty()==False:
        parrent_node=pq_open.get()
        if parrent_node[0]==_goal:
            break
        q_close.put(parrent_node[0])
        for i in range(20):
            length=graph.get_edge(parrent_node[0],i)
            if length!=0:
                node,result=pq_open.contain(i)
                f=parrent_node[1]-h[parrent_node[0]]+length+h[i]
                if q_close.contain(i):
                    continue
                elif result==True:
                    if node[1]>f:
                        node.pop()
                        parents[i]=parrent_node[0]
                        pq_open.put([i,f])
                else:
                    parents[i]=parrent_node[0]
                    pq_open.put([i,f])
    path=[]
    cost=0
    print("parents:",parents)
    p=_goal

    while p!=_root:
        cost+=graph.get_edge(p,parents[p])
        path.append(p);
        p=parents[p]

    length=len(path)-1
    print('path:',_root,end='')
    for i in range(length+1):
        print(" -->",path[length-i],end='')
    print()
    return cost
    

if __name__ == '__main__':
    graph = init_graph()
    # 怎麼才能夠獲取到這個矩陣
    h = (366, 0, 160, 242, 161,
         178, 77, 151, 226, 244,
         241, 234, 380, 98, 193,
         253, 329, 80, 199, 374)

    # place_str = input()
    # places = place_str.split()
    # cost = a_star(graph, h, eval(places[0]), eval(places[1]))

    print("node","A"," B")
    cost = a_star(graph, h,eval('A'),eval('B'))
    print('cost:',cost)
    print()
    print("node","C"," B")
    cost = a_star(graph, h,eval('C'),eval('B'))
    print('cost:',cost)
    print()
    print("node","U"," B")
    cost = a_star(graph, h,eval('U'),eval('B'))
    print('cost:',cost)
    print()

備註:

  1. 保存好源碼之後,自己多寫幾個print來看一看各個數據都是什麼,避免超級低級的錯誤。
  2. 源碼總註釋比較少,爲何?因爲有了流程圖,以及上面的展示,A*算法的思想還是很容易就可以理解的,而且算法部分的代碼很容易就能夠看懂。
  3. 話說我也不知道是爲什麼,我上傳代碼之後竟然沒有高亮顯示,感覺很奇怪,是不是我等級尚低,修爲尚淺,還是因爲之前的代碼都是MATLAB代碼。
  4. 代碼最後的部分,註釋掉的三行是用來控制檯輸入的,但是鑑於Sublime Text3中input()函數不好用,我直接弄成了固定的測試數據。
  5. 代碼中最後的cost計算,可以直接從A*算法中得到,並不需要使用回溯的方法,所以我這裏這樣寫,純屬畫蛇添足,但是因爲要輸出路徑,所以順帶算了一下

5.算法輸出

在這裏插入圖片描述

6.總結

不難。但是卻囿於有限的知識,而不能夠將其進行擴展,A*算法是一種比較好的尋路算法,在遊戲中會經常使用到,在Unity中有特定的尋路算法,也有一部分人是自己寫的算法,自己寫的話有什麼好處呢?
倘若是自己寫的尋路算法,在尋路的時候,可以給AI加上好多東西,這樣可以讓AI看上去不是那麼傻?現在還是有一些疑惑的,尋路的時候是將地圖當成了一個類似於座標系一樣的東西,中間遇到障礙物就會換路,而且是找到一條路徑之後,AI纔會出發,也就是說AI提前就知道一條路徑,o(  ̄︶ ̄ )o。
那麼怎麼把現在的這種數據跟遊戲中尋路數據對應起來呢?好暈。找了一些個鏈接,有時間的時候可以看一看,或者就最近這一段時間看一看

鏈接如下:

  1. A星算法詳解(個人認爲最詳細,最通俗易懂的一個版本)
  2. A* Pathfinding for Beginners(不用外網)
  3. 堪稱最好最全的A*算法詳解(譯文)
  4. Amit’s A* Pages(不用外網)

應該嘗試用各種編程語言寫該算法,因爲不能容忍自己的菜!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章