1. 程式人生 > 實用技巧 >啟發式搜尋——A*演算法

啟發式搜尋——A*演算法

簡介

最近在學習啟發式搜尋,看到一篇關於A*演算法的文章,我覺得寫得很好,就搬運過來學習記錄一下,英文版原文在此有興趣的可以看一下。

搜尋區域(The Search Area)

我們假設某人要從 A 點移動到 B 點,但是這兩點之間被一堵牆隔開。如圖 1 ,綠色是 A ,紅色是 B ,中間藍色是牆。

你應該注意到了,我們把要搜尋的區域劃分成了正方形的格子。這是尋路的第一步,簡化搜尋區域,就像我們這裡做的一樣。這個特殊的方法把我們的搜尋區域簡化為了 2 維陣列。陣列的每一項代表一個格子,它的狀態就是可走 (walkalbe) 和不可走 (unwalkable) 。通過計算出從 A 到 B需要走過哪些方格,就找到了路徑。一旦路徑找到了,人物便從一個方格的中心移動到另一個方格的中心,直至到達目的地。

方格的中心點我們成為“節點 (nodes) ”。如果你讀過其他關於 A* 尋路演算法的文章,你會發現人們常常都在討論節點。為什麼不直接描述為方格呢?因為我們有可能把搜尋區域劃為為其他多變形而不是正方形,例如可以是六邊形,矩形,甚至可以是任意多變形。而節點可以放在任意多邊形裡面,可以放在多變形的中心,也可以放在多邊形的邊上。我們使用這個系統,因為它最簡單。

一旦我們把搜尋區域簡化為一組可以量化的節點後,就像上面做的一樣,我們下一步要做的便是查詢最短路徑。在 A* 中,我們從起點開始,檢查其相鄰的方格,然後向四周擴充套件,直至找到目標。

我們這樣開始我們的尋路旅途:

  1.   從起點 A 開始,並把它就加入到一個由方格組成的 open list( 開放列表 ) 中。這個 open list 有點像是一個購物單。當然現在 open list 裡只有一項,它就是起點 A ,後面會慢慢加入更多的項。 Open list 裡的格子是路徑可能會是沿途經過的,也有可能不經過。基本上 open list 是一個待檢查的方格列表。
    
  2.   檢視與起點 A 相鄰的方格 ( 忽略其中牆壁所佔領的方格,河流所佔領的方格及其他非法地形佔領的方格 ) ,把其中可走的 (walkable) 或可到達的 (reachable) 方格也加入到 open list 中。把起點 A 設定為這些方格的父親 (parent node 或 parent square) 。當我們在追蹤路徑時,這些父節點的內容是很重要的。稍後解釋。
    
  3.   把 A 從 open list 中移除,加入到 close list( 封閉列表 ) 中, close list 中的每個方格都是現在不需要再關注的。
    

如下圖所示,深綠色的方格為起點,它的外框是亮藍色,表示該方格被加入到了 close list 。與它相鄰的黑色方格是需要被檢查的,他們的外框是亮綠色。每個黑方格都有一個灰色的指標指向他們的父節點,這裡是起點 A 。

下一步,我們需要從 open list 中選一個與起點 A 相鄰的方格,按下面描述的一樣或多或少的重複前面的步驟。但是到底選擇哪個方格好呢?具有最小 F 值的那個。

路徑排序(Path Sorting)

計算出組成路徑的方格的關鍵是下面這個等式:

F = G + H

這裡,

G = 從起點 A 移動到指定方格的移動代價,沿著到達該方格而生成的路徑。

H = 從指定的方格移動到終點 B 的估算成本。這個通常被稱為試探法,有點讓人混淆。為什麼這麼叫呢,因為這是個猜測。直到我們找到了路徑我們才會知道真正的距離,因為途中有各種各樣的東西 ( 比如牆壁,水等 ) 。本教程將教你一種計算 H 的方法,你也可以在網上找到其他方法。

我們的路徑是這麼產生的:反覆遍歷 open list ,選擇 F 值最小的方格。這個過程稍後詳細描述。我們還是先看看怎麼去計算上面的等式。

如上所述, G 是從起點A移動到指定方格的移動代價。在本例中,橫向和縱向的移動代價為 10 ,對角線的移動代價為 14 。之所以使用這些資料,是因為實際的對角移動距離是 2 的平方根,或者是近似的 1.414 倍的橫向或縱向移動代價。使用 10 和 14 就是為了簡單起見。比例是對的,我們避免了開放和小數的計算。這並不是我們沒有這個能力或是不喜歡數學。使用這些數字也可以使計算機更快。稍後你便會發現,如果不使用這些技巧,尋路演算法將很慢。

既然我們是沿著到達指定方格的路徑來計算 G 值,那麼計算出該方格的 G 值的方法就是找出其父親的 G 值,然後按父親是直線方向還是斜線方向加上 10 或 14 。隨著我們離開起點而得到更多的方格,這個方法會變得更加明朗。

有很多方法可以估算 H 值。這裡我們使用 Manhattan 方法,計算從當前方格橫向或縱向移動到達目標所經過的方格數,忽略對角移動,然後把總數乘以 10 。之所以叫做 Manhattan 方法,是因為這很像統計從一個地點到另一個地點所穿過的街區數,而你不能斜向穿過街區。重要的是,計算 H 是,要忽略路徑中的障礙物。這是對剩餘距離的估算值,而不是實際值,因此才稱為試探法。

把 G 和 H 相加便得到 F 。我們第一步的結果如下圖所示。每個方格都標上了 F , G , H 的值,就像起點右邊的方格那樣,左上角是 F ,左下角是 G ,右下角是 H 。

好,現在讓我們看看其中的一些方格。在標有字母的方格, G = 10 。這是因為水平方向從起點到那裡只有一個方格的距離。與起點直接相鄰的上方,下方,左方的方格的 G 值都是 10 ,對角線的方格 G 值都是 14 。

H 值通過估算起點於終點 ( 紅色方格 ) 的 Manhattan 距離得到,僅作橫向和縱向移動,並且忽略沿途的牆壁。使用這種方式,起點右邊的方格到終點有 3 個方格的距離,因此 H = 30 。這個方格上方的方格到終點有 4 個方格的距離 ( 注意只計算橫向和縱向距離 ) ,因此 H = 40 。對於其他的方格,你可以用同樣的方法知道 H 值是如何得來的。

每個方格的 F 值,再說一次,直接把 G 值和 H 值相加就可以了。

為了繼續搜尋,我們從 open list 中選擇 F 值最小的 ( 方格 ) 節點,然後對所選擇的方格作如下操作:

  1.   把它從 open list 裡取出,放到 close list 中。
    
  2.   檢查所有與它相鄰的方格,忽略其中在 close list 中或是不可走 (unwalkable) 的方格 ( 比如牆,水,或是其他非法地形 ) ,如果方格不在open lsit 中,則把它們加入到 open list 中。
    

把我們選定的方格設定為這些新加入的方格的父親。

  1.   如果某個相鄰的方格已經在 open list 中,則檢查這條路徑是否更優,也就是說經由當前方格 ( 我們選中的方格 ) 到達那個方格是否具有更小的 G 值。如果沒有,不做任何操作。
    

相反,如果 G 值更小,則把那個方格的父親設為當前方格 ( 我們選中的方格 ) ,然後重新計算那個方格的 F 值和 G 值。如果你還是很混淆,請參考下圖。

Ok ,讓我們看看它是怎麼工作的。在我們最初的 9 個方格中,還有 8 個在 open list 中,起點被放入了 close list 中。在這些方格中,起點右邊的格子的 F 值 40 最小,因此我們選擇這個方格作為下一個要處理的方格。它的外框用藍線打亮。

首先,我們把它從 open list 移到 close list 中 ( 這就是為什麼用藍線打亮的原因了 ) 。然後我們檢查與它相鄰的方格。它右邊的方格是牆壁,我們忽略。它左邊的方格是起點,在 close list 中,我們也忽略。其他 4 個相鄰的方格均在 open list 中,我們需要檢查經由這個方格到達那裡的路徑是否更好,使用 G 值來判定。讓我們看看上面的方格。它現在的 G 值為 14 。如果我們經由當前方格到達那裡, G 值將會為 20(其中 10 為到達當前方格的 G 值,此外還要加上從當前方格縱向移動到上面方格的 G 值 10) 。顯然 20 比 14 大,因此這不是最優的路徑。如果你看圖你就會明白。直接從起點沿對角線移動到那個方格比先橫向移動再縱向移動要好。

當把 4 個已經在 open list 中的相鄰方格都檢查後,沒有發現經由當前方格的更好路徑,因此我們不做任何改變。現在我們已經檢查了當前方格的所有相鄰的方格,並也對他們作了處理,是時候選擇下一個待處理的方格了。

因此再次遍歷我們的 open list ,現在它只有 7 個方格了,我們需要選擇 F 值最小的那個。有趣的是,這次有兩個方格的 F 值都 54 ,選哪個呢?沒什麼關係。從速度上考慮,選擇最後加入 open list 的方格更快。這導致了在尋路過程中,當靠近目標時,優先使用新找到的方格的偏好。但是這並不重要。 ( 對相同資料的不同對待,導致兩中版本的 A* 找到等長的不同路徑 ) 。

我們選擇起點右下方的方格,如下圖所示。

這次,當我們檢查相鄰的方格時,我們發現它右邊的方格是牆,忽略之。上面的也一樣。

我們把牆下面的一格也忽略掉。為什麼?因為如果不穿越牆角的話,你不能直接從當前方格移動到那個方格。你需要先往下走,然後再移動到那個方格,這樣來繞過牆角。 ( 注意:穿越牆角的規則是可選的,依賴於你的節點是怎麼放置的 )

這樣還剩下 5 個相鄰的方格。當前方格下面的 2 個方格還沒有加入 open list ,所以把它們加入,同時把當前方格設為他們的父親。在剩下的3 個方格中,有 2 個已經在 close list 中 ( 一個是起點,一個是當前方格上面的方格,外框被加亮的 ) ,我們忽略它們。最後一個方格,也就是當前方格左邊的方格,我們檢查經由當前方格到達那裡是否具有更小的 G 值。沒有。因此我們準備從 open list 中選擇下一個待處理的方格。

不斷重複這個過程,直到把終點也加入到了 open list 中,此時如下圖所示。

注意,在起點下面 2 格的方格的父親已經與前面不同了。之前它的 G 值是 28 並且指向它右上方的方格。現在它的 G 值為 20 ,並且指向它正上方的方格。這在尋路過程中的某處發生,使用新路徑時 G 值經過檢查並且變得更低,因此父節點被重新設定, G 和 F 值被重新計算。儘管這一變化在本例中並不重要,但是在很多場合中,這種變化會導致尋路結果的巨大變化。

那麼我們怎麼樣去確定實際路徑呢?很簡單,從終點開始,按著箭頭向父節點移動,這樣你就被帶回到了起點,這就是你的路徑。如下圖所示。從起點 A 移動到終點 B 就是簡單從路徑上的一個方格的中心移動到另一個方格的中心,直至目標。就是這麼簡單!

A演算法總結(Summary of the A Method)

Ok ,現在你已經看完了整個的介紹,現在我們把所有步驟放在一起:

  1.     把起點加入 open list 。
    
  2.     重複如下過程:
    

a. 遍歷 open list ,查詢 F 值最小的節點,把它作為當前要處理的節點。

b. 把這個節點移到 close list 。

c. 對當前方格的 8 個相鄰方格的每一個方格?

◆ 如果它是不可抵達的或者它在 close list 中,忽略它。否則,做如下操作。

◆ 如果它不在 open list 中,把它加入 open list ,並且把當前方格設定為它的父親,記錄該方格的 F , G 和 H 值。

◆ 如果它已經在 open list 中,檢查這條路徑 ( 即經由當前方格到達它那裡 ) 是否更好,用 G 值作參考。更小的 G 值表示這是更好的路徑。如果是這樣,把它的父親設定為當前方格,並重新計算它的 G 和 F 值。如果你的 open list 是按 F 值排序的話,改變後你可能需要重新排序。

d. 停止,當你

◆ 把終點加入到了 open list 中,此時路徑已經找到了,或者

◆ 查詢終點失敗,並且 open list 是空的,此時沒有路徑。

  1.     儲存路徑。從終點開始,每個方格沿著父節點移動直至起點,這就是你的路徑。