1. 程式人生 > 其它 >淺析路徑規劃中的A-star演算法及其實現

淺析路徑規劃中的A-star演算法及其實現

可訪問我的知乎:https://zhuanlan.zhihu.com/p/478858388

圖搜尋簡介

圖搜尋總能產生一棵搜尋樹,高效+最優構建搜尋樹為演算法核心。

圖搜尋演算法一般框架如下所示:

盲目搜尋方法

所有的圖搜尋演算法都具有一種容器(container)和一種方法(algorithm)

  • “容器”在一定意義上就是開集,確定了演算法的資料結構基礎,以起始節點\(S\)​初始化,定義了結點進出的規則,深搜就是棧(stack),廣搜就是佇列(queue)
  • “方法”確定了結點彈出的順序,深搜(Depth First Search)中是“彈出最深的節點”,廣搜(Breadth First Search)中是“彈出最淺的節點
    ”(在樹中表現為由根向葉層序推進)。

需要注意的是,DFS不能保證在一定的時空複雜度限制下尋找到最短路徑。因此,圖搜尋的基礎是BFS

啟發式方法

一般地,BFS只適用於任意兩點距離為1的圖搜尋找最短路徑,而且屬於“撒網式”的沒有明確目標方向的盲目嘗試。在BFS的基礎上,重新定義結點出棧的順序“具有最優屬性的結點先彈出容器”,並升級容器為“優先佇列”,就形成了具有啟發式的路徑搜尋演算法。
在正邊權有向圖中,每個節點的距離代價評估可用估價函式\({f(n)}\)來建模。

\[f(n)=g(n)+h(n)\\ \]

其中\(g(n)\)是在狀態空間中從初始節點到節點\(n\)的實際代價,\(h(n)\)

是從節點\(n\)​到目標節點最佳路徑的啟發式估計代價,即“啟發式(heuristic)距離”,“猜測”當前節點距離目標節點還有多遠。

  • Greedy:\(f(n)=h(n)\)​​​​
    • 策略:不斷訪問距終點啟發距離最小的鄰點(預設當前點到所有鄰點距離相同,不同則加上當前點到鄰點距離代價)
    • 無障礙情況下比BFS高效;在最短路徑上出現障礙則極大可能找不到最優解。
  • Dijkstra:\(f(n)=g(n)\)
    • 策略:不斷訪問距原點累計距離最小的鄰點,鄰點若未擴充套件過直接加入優先佇列,若已擴充套件過(即在優先佇列中)則進行鬆弛

    • 最優性保證:已擴充套件點儲存的一定是距離起始點的最短距離

    • 搜尋過程均勻擴充套件(與邊權相關),若任意兩點距離為1退化為BFS

    • 虛擬碼如下:

    • Dijkstra與Greedy演算法的對比如下:

  • A-star:\({f(n)=g(n)+h(n)}\)
    • A*演算法與Dijstra等一致代價搜尋演算法的主要區別在於啟發項\({h(n)}\)的存在將優先佇列的排序依據由\(g(n)\)變成\(f(n)\)

      A-star程式設計注意更新時要同步更新優先佇列中每個節點的\(g(n)\)

    • 估價距離\({h(n)}\)不大於節點\(n\)​​到目標節點的距離時,搜尋的點數多、範圍大、效率低,保證得到最優解;若估價距離大於實際距離, 搜尋的點數少、範圍小、效率高,但不能保證得到最優解。估價值與實際值越接近,估價函式質量越高。

    • \(h\le h^*\)​​時保證演算法完備性,舉例如下:

    • 虛擬碼如下:

A-star演算法流程

A*演算法是靜態路網中求解最短路最有效的方法之一,主要搜尋過程虛擬碼示意如下:

//step 1
建立兩個表,OPEN表儲存所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點。
//step 2
遍歷當前節點的各個節點,將n節點放入CLOSE中,取n節點的子節點X,算X的估價值
//step 3
While(OPEN!=NULL)
   {
    從OPEN表中取估價值f最小的節點n;
   	if(n節點==目標節點) break;
   	else
   	{
   		if(X in OPEN) 
            比較兩個X的估價值f //注意是同一個節點的兩個不同路徑的估價值
   		if( X的估價值小於OPEN表的估價值 )
            更新OPEN表中的估價值; //取最小路徑的估價值
		if(X in CLOSE) 
            比較兩個X的估價值 //注意是同一個節點的兩個不同路徑的估價值
		if( X的估價值小於CLOSE表的估價值 )
   		  更新CLOSE表中的估價值; 把X節點放入OPEN //取最小路徑的估價值
		if(X not in both)
			求X的估價值;並將X插入OPEN表中; //還沒有排序
	}
	將n節點插入CLOSE表中;按照估價值將OPEN表中的節點排序; 
    //(實際上是比較OPEN表內節點f的大小,從最小路徑的節點向下進行。)
}

A*演算法框圖展示如下:

A-star演算法實現

[編譯環境]

Windows 系統|PyCharm 編譯器|python 3.8.11

定義地圖類

由長度、寬度、起點座標、終點座標、障礙座標列表、地圖模式(4鄰接模式/8鄰接模式)唯一確定一個地圖類。

[4鄰接模式]:

agent所有可能的移動範圍包括上、下、左、右四個方向,一步行進一個單位長度

[8鄰接模式]:

agent所有可能的移動範圍包括上、下、左、右、左上、左下、右上、右下八個方向,一步行進一個單位長度

class Map:
    def __init__(self, width, height, start, end, obstacles, mode):
        assert mode == 4 or mode == 8
        self.OBSTACLE = -1
        self.START = 1
        self.END = 2
        self.start = start
        self.end = end
        self.height = height
        self.width = width
        self.mode = mode
        # --------------------------------------------------
        self.mp = np.zeros((height, width))
        # set begin and end
        self.mp[start] = self.START
        self.mp[end] = self.END
        # set obstacles
        for x, y in obstacles:
            self.mp[x, y] = self.OBSTACLE

A*演算法類

繼承地圖類的資訊,類內成員變數和函式具體闡釋如下:

class Solver(Map):
    def __init__(self, width, height, start, end, obstacles, mode):
        super(Solver, self).__init__(width, height, start, end, obstacles, mode)
        self.mindistance = inf
        self.path = []

    def within(self, x, y):  # border detection
        return 0 <= x < self.height and 0 <= y < self.width

    def neighbors(self, node):  # get neighbors
        if self.mode == 4:
            direction = [(-1, 0), (0, -1), (0, 1), (1, 0)]
        if self.mode == 8:
            direction = [(-1, 0), (0, -1), (0, 1), (1, 0),
                         (-1, -1), (1, -1), (-1, 1), (1, 1)]
        return [(node[0] + x, node[1] + y) for (x, y) in direction if
                self.within(node[0] + x, node[1] + y) and self.mp[node[0] + x, node[1] + y] != self.OBSTACLE]

    def movecost(self, cur, near):  # move cost,移動距離由mode決定
        if self.mode == 8:
            ord = np.inf
        if self.mode == 4:
            ord = 1
        return np.linalg.norm(np.array(cur) - np.array(near), ord=ord)

    def heuristic(self, near, end):  # heuristic distance,啟發式距離可人為設定,預設曼哈頓距離
        # 當mode = 4, ord = 1 / 2 / inf
        # 當mode = 8, ord = inf
        if self.mode == 8:
            ord = np.inf
        if self.mode == 4:
            ord = np.random.choice([1, 2, np.inf])
        return np.linalg.norm(np.array(end) - np.array(near), ord=ord)

    def A_star(self):  # search
        # init priority-queue
        q = PriorityQueue()
        q.put(self.start, int(0))
        # init path recorder
        comeFrom = {self.start: None}
        # init current cost recorder
        costSoFar = {self.start: 0}
        # searching
        while q.qsize():
            cur = q.get()
            if cur == self.end:
                break
            for near in self.neighbors(cur):
                newCost = costSoFar[cur] + self.movecost(cur, near)
                if near not in costSoFar or newCost < costSoFar[near]:  # 沒有搜過的點相當於距離無窮大
                    costSoFar[near] = newCost
                    comeFrom[near] = cur
                    q.put(near, costSoFar[near] + self.heuristic(near, self.end))

        # terminate,find path recursively
        terminal = self.end
        path = [self.end]
        while comeFrom.get(terminal, None) is not None:
            path.append(comeFrom[terminal])
            terminal = comeFrom[terminal]
        path.reverse()
        self.mindistance = costSoFar.get(self.end, inf)
        self.path = path

    def outputresult(self):
        mindistance = self.mindistance if self.mindistance != inf else '∞'
        print(f'從{self.start}到{self.end}最短距離:{mindistance}')
        print('最短路徑如下:')
        if len(self.path) == 1 and self.path[0] == end:
            print('empty path')
        else:
            for i, node in enumerate(self.path):
                print(node, end='')
                if i != len(self.path) - 1:
                    print('->', end='')
                else:
                    print()

資料匯入

def loadTestData(n=1):
    if n == 1:
        # 起始點
        start = (2, 2)
        end = (6, 12)
        # 建立障礙
        obstacle_y = [i for i in range(5, 10)]
        obstacle_x = [2] * len(obstacle_y)
        tmp = [i for i in range(3, 6)]
        obstacle_x.extend(tmp)
        obstacle_y.extend([9] * len(tmp))
        tmp = [i for i in range(5, 10)]
        obstacle_y.extend(tmp)
        obstacle_x.extend([6] * len(tmp))
        obstacles = zip(obstacle_x, obstacle_y)
    if n == 2:
        start = (0, 0)
        end = (3, 3)
        obstacles = [(3, 2), (3, 4), (2, 3), (4, 3)]
    return start, end, obstacles

主函式

if __name__ == '__main__':
    # 初始化地圖基本屬性
    WIDTH = 15
    HEIGHT = 10
    mode = 4
    start, end, obstacles = loadTestData(n=1)
    print(f'起點:{start} 終點:{end}')
    print('障礙:', *obstacles)
    print('------------------------------------------------------')

    # A*最短路徑求解
    A_star_solver = Solver(WIDTH, HEIGHT, start, end, obstacles, mode)
    A_star_solver.A_star()
    A_star_solver.outputresult()

控制檯測試

  • 正常情況測試(存在最短路徑)
  • 異常情況測試(4-鄰接下無最短路徑)

A-star演算法視覺化呈現

引入PythonPyQt5第三方庫,主要通過自行實現GameBoard類搭建視窗程式,完成A*演算法在地圖尋路上的應用(具體程式碼詳見附件)。

視窗的主要區域為地圖視覺化顯示,地圖右側分別展示視窗的使用說明、地圖的顏色說明、操作功能鍵以及資訊輸出。通過載入預測地圖或者根據使用說明設定地圖後即可點選“開始搜尋”進行尋路結果演示,演算法尋找到的最優路徑以及路徑的最短距離在尋路演示之後會呈現在資訊輸出區域。

[使用說明]

右鍵 : 首次單擊格子選定起始點,第二次單擊格子選定終點

左鍵 : 選定格子為牆壁,單擊牆壁則刪除牆壁

[顏色說明]

黃色 : 代表起點

綠色 : 代表終點

黑色 : 代表牆壁

灰色 : 代表可行區域

紅色 : 閃爍,代表最短路徑上的每個節點

視訊演示中我們分別使智慧體以4鄰接和8鄰接方式進行尋路,所使用的地圖如上所示。結果比較如下:

# 4鄰接
從(0, 0)到(12, 15)最短距離:41
最短路徑如下:
(0, 0)->(1, 0)->(2, 0)->(3, 0)->(4, 0)->(5, 0)->(6, 0)->(6, 1)->(6, 2)->(5, 2)->(4, 2)->(3, 2)->(2, 2)->(1, 2)->(0, 2)->(0, 3)->(0, 4)->(0, 5)->(0, 6)->(0, 7)->(0, 8)->(0, 9)->(1, 9)->(2, 9)->(3, 9)->(4, 9)->(5, 9)->(6, 9)->(6, 10)->(6, 11)->(5, 11)->(5, 12)->(5, 13)->(5, 14)->(5, 15)->(6, 15)->(7, 15)->(8, 15)->(9, 15)->(10, 15)->(11, 15)->(12, 15)

# 8鄰接
從(0, 0)到(12, 15)最短距離:21
最短路徑:
(0, 0)->(1, 0)->(2, 0)->(3, 0)->(4, 0)->(5, 0)->(6, 0)->(7, 1)->(8, 2)->(9, 3)->(10, 4)->(11, 5)->(10, 6)->(11, 7)->(12, 8)->(12, 9)->(12, 10)->(12, 11)->(12, 12)->(12, 13)->(12, 14)->(12, 15)

應用A*演算法在自己設計的遊戲介面上執行順利,我們繼續探索,將演算法應用在真實遊戲中,實現功能:通過滑鼠點選目標位置使遊戲人物以最短路徑到達指定位置。結果呈現見視訊演示。

評價

  • A*演算法的核心程式碼部分主要基於優先佇列的資料結構實現(底層結構為二叉堆),既凸顯啟發式演算法的特徵,在程式碼效率方面相比其他資料結構又有一定的提升;同時考慮到無最短路徑的特殊情況,演算法魯棒性強。
  • A*演算法的核心程式碼以及視覺化程式碼通過類進行封裝並形成一個完整模組,便於改變地圖模式,也便於程式碼的維護與除錯。

擴充套件

地圖路標形式

將地圖的拓撲特徵抽取出,使用路標形式儲存地圖可以有效提高演算法尋路的效率。

A-star演算法工程應用

​ 從更加巨集觀和一般的角度看待含有啟發式資訊的尋路演算法:

Weighted A-star:\(f(n)=g(n)+\epsilon h(n),\epsilon > 1\)

  • 用次優解換取更少的搜尋時間,高估的啟發距離使其更偏向貪心演算法,可證明次優解質量滿足:\(cost\le \epsilon·cost^*\)
  • 還可以使\(\epsilon\)隨搜尋越來越接近1,在最優性和時間成本之間權衡
  • 最合適的啟發式函式

    由於h越接近h*越好,而在無障礙的柵格地圖中最短路徑一定沿以起點終點確定的矩形的對角線,因此可定義Diagonal Heuristic:

    # D為水平/豎直移動代價;D2為斜線移動代價
    def heuristic(node,goal):
        dx = abs(node.x - goal.x)
        dy = abs(node.y - goal.y)
        return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)
    
  • 打破路徑的對稱性以減少搜尋次數
    對於f相等的路徑,A*中是無差別探索,結果趨向於找到多條最優路徑。但實際上只需要一條,因此在搜尋的時候可以設定“傾向”,僅找一條最短路徑,思路可以選擇如下幾種:

    • 在f相同時選擇h大/小的路線
    • 構建一張僅與座標關聯的隨機數表,\(h=h+\epsilon\)
    • 趨向選擇更接近對角線的路線
      def h_(start,node,goal):
      	dx1 = abs(node.x - goal.x)
          dy1 = abs(node.y - goal.y)
          dx2 = abs(start.x - goal.x)
          dy2 = abs(start.y - goal.y)
          cross = abs(dy2*dx1-dx2*dy1)
          return h(node,goal) + cross * 0.001 
      	# h為原啟發式函式,cross愈大相當於當前點離對角線上的點越遠對原		 h給與更高的懲罰,cross的係數必須小不要逾越h_<=h*的最優條件
      
    • 稍微“打破”完備性條件

D-star演算法淺談

A演算法是靜態路網中有效的尋路演算法,而D**演算法是不斷變化的動態環境下采用的有效尋路演算法,其主要演算法流程如下:

//step 1
先用Dijstra演算法從目標節點G向起始節點搜尋。儲存路網中目標點到各個節點的最短路和該位置到目標點的實際值h,k。(k為所有變化h之中最小的值,當前為k=h。每個節點包含上一節點到目標點的最短路資訊1(2),2(5),5(4),4(7)。則1到4的最短路為1-2-5-4)
原OPEN和CLOSE中節點資訊儲存。
//step 2
/機器人沿最短路開始移動,在移動的下一節點沒有變化時,無需計算,利用上一步Dijstra計算出的最短路資訊從出發點向後追述即可,當在Y點探測到下一節點X狀態發生改變(如堵塞)。機器人首先調整自己在當前位置Y到目標點G的實際值h(Y),h(Y)=X到Y的新權值c(X,Y)+X的原實際值h(X).X為下一節點(到目標點方向Y->X->G),Y是當前點。k值取h值變化前後的最小。
//step 3
用A*或其它演算法計算,這裡假設用A*演算法,遍歷Y的子節點,點放入CLOSE,調整Y的子節點a的h值,h(a)=h(Y)+Y到子節點a的權重C(Y,a),比較a點是否存在於OPEN和CLOSE中,
//偽碼示意
while()
{
 從OPEN表中取k值最小的節點Y;
 遍歷Y的子節點a,計算a的h值 h(a)=h(Y)+Y到子節點a的權重C(Y,a)
 {
     if(a in OPEN)     
         比較兩個a的h值 
     if( a的h值小於OPEN表a的h值 )
     {
      	更新OPEN表中a的h值;k值取最小的h值
         有未受影響的最短路經存在
         break; 
     }
     if(a in CLOSE) 
         比較兩個a的h值 //注意是同一個節點的兩個不同路徑的估價值
     if( a的h值小於CLOSE表的h值 )
     {
      	更新CLOSE表中a的h值; k值取最小的h值;將a節點放入OPEN表
         有未受影響的最短路經存在
         break;
     }
     if(a not in both)
         將a插入OPEN表中; //還沒有排序
 }
 放Y到CLOSE表;
 OPEN表比較k值大小進行排序;
}
機器人利用第一步Dijstra計算出的最短路資訊從a點到目標點的最短路經進行。

總結

本文從圖搜尋到A*演算法從理論到實踐分析比較了A-star演算法的優勢並給出其程式碼實現,並且進一步探討了路標形式表示優化演算法效率的方法以及瞭解了應用於動態環境下的D-star演算法,為更復雜問題的尋路搜尋提供了思路。