經典演算法研究系列:一、A*搜尋演算法
July 二零一一年一月
==============
博主說明:
1、此經典演算法研究系列,此系列文章寫的不夠好之處,還望見諒。
2、本經典演算法研究系列,系我參考資料,一篇一篇原創所作,轉載必須註明作者本人July及出處。
3、本經典演算法研究系列,精益求精,不斷最佳化,永久更新,永久勘誤。
歡迎,各位,與我一同學習探討,交流研究。
有誤之處,不吝指正。
-------------------------------------------
A* 搜尋演算法
1968年,的一篇論文,“P. E. Hart, N. J. Nilsson, and B. Raphael. A formal basis for the heuristic determination of minimum cost paths in graphs. IEEE Trans. Syst. Sci. and Cybernetics, SSC-4(2):100-107, 1968”。從此,一種精巧、高效的演算法------A*演算法橫空出世了,並在相關領域得到了廣泛的應用。
DFS和BFS在展開子結點時均屬於盲目型搜尋,也就是說,它不會選擇哪個結點在下一次搜尋中更優而去跳轉到該結點進行下一步的搜尋。在運氣不好的情形中,均需要試探完整個解集空間, 顯然,只能適用於問題規模不大的搜尋問題中。
那麼,作為啟發學習法演算法中的A*演算法,又比它們高效在哪裡呢?
首先要來談一下什麼是啟發學習法演算法。
所謂啟發學習法搜尋,與DFS和BFS這類盲目型搜尋最大的不同,就在於當前搜尋結點往下選擇下一步結點時,可以通過一個啟發函數來進行選擇,選擇代價最少的結點作為下一步搜尋結點而跳轉其上(遇到有一個以上代價最少的結點,不妨選距離當前搜尋點最近一次展開的搜尋點進行下一步搜尋)。
一個經過仔細設計的啟發函數,往往在很快的時間內就可得到一個搜尋問題的最優解,
對於NP問題,亦可在多項式時間內得到一個較優解。
是的,關鍵就是如何設計這個啟發函數。
A*演算法,作為啟發學習法演算法中很重要的一種,被廣泛應用在最優路徑求解和一些策略設計的問題中。
而A*演算法最為核心的部分,就在於它的一個估值函數的設計上:
f(n)=g(n)+h(n)
其中f(n)是每個可能試探點的估值,它有兩部分組成:
一部分,為g(n),它表示從起始搜尋點到當前點的代價(通常用某結點在搜尋樹中的深度來表示)。
另一部分,即h(n),它表示啟發學習法搜尋中最為重要的一部分,即當前結點到目標結點的估值,
h(n)設計的好壞,直接影響著具有此種啟發學習法函數的啟發學習法演算法的是否能稱為A*演算法。
一種具有f(n)=g(n)+h(n)策略的啟發學習法演算法能成為A*演算法的充分條件是:
1、搜尋樹上存在著從起始點到終了點的最優路徑。
2、問題域是有限的。
3、所有結點的子結點的搜尋代價值>0。
4、h(n)=<h*(n) (h*(n)為實際問題的代價值)。
當此四個條件都滿足時,一個具有f(n)=g(n)+h(n)策略的啟發學習法演算法能成為A*演算法,並一定能找到最優解。
對於一個搜尋問題,顯然,條件1,2,3都是很容易滿足的,而條件4: h(n)<=h*(n)是需要精心設計的,由於h*(n)顯然是無法知道的,所以,一個滿足條件4的啟發策略h(n)就來的難能可貴了。
不過,對於圖的最優路徑搜尋和八數位問題,有些相關策略h(n)不僅很好理解,而且已經在理論上證明是滿足條件4的,從而為這個演算法的推廣起到了決定性的作用。
且h(n)距離h*(n)的呈度不能過大,否則h(n)就沒有過強的區分能力,演算法效率並不會很高。對一個好的h(n)的評價是:h(n)在h*(n)的下界之下,並且盡量接近h*(n).
維基百科對本A*搜尋演算法的解釋:
A*搜尋演算法,俗稱A星演算法。這是一種在圖形平面上,有多個節點的路徑,求出最低通過成本的演算法。
常用於遊戲中的NPC的移動計算,或線上遊戲的BOT的移動計算上。
該演算法像Dijkstra演算法一樣,可以找到一條最短路徑;也像BFS一樣,進行啟發學習法的搜尋。
在此演算法中,
g(n)表示從起點到任意頂點n的實際距離,
h(n)表示任意頂點n到目標頂點的估算距離。
//即,起點-->任意頂點g(n)-->目標頂點h(n)
因此,A*演算法的公式為:
f(n)=g(n)+h(n)。
這個公式遵循以下特性:
I、如果h(n)為0,只需求出g(n),即求出起點到任意頂點n的最短路徑,
則轉化為單源最短路徑問題,即Dijkstra演算法。
II、如果h(n)<=n到目標的實際距離,則一定可以求出最優解。而且h(n)越小,需要計算的節點越多,演算法效率越低。
A*搜尋演算法的演算法實現:
A*是求這樣一個和最短路徑有關的問題,那單純的最短路徑問題當然可以用A*來算,對於g(n)就是[S,n],在搜尋過程中計算,而h(n)我想不出很好的辦法,對於一個抽象的圖搜尋,很難找到很好的h(n),因為h(n)和具體的問題有關。只好是h(n)=0,退為有序搜尋,舉一個小小的例子:
V0->V1,V2,V3,V4,V5的路徑之和。即好比一個人從V0點出發,要達到V1..V5,五個點。圖中達到過的點,可以作為到達其它點的連通橋。
下面的問題,即是求此起始頂點V0->途徑任意頂點V1、V2、V3、V4->目標頂點V5的最短路徑。
//是的,圖片是引用rickone 的。
通過,我們可以看出:
A*演算法最為核心的過程,就在每次選擇下一個當前搜尋點時,是從所有已探知的但未搜尋過點中(可能是不同層,亦可不在同一條支路上),選取f值最小的結點進行展開。
而所有“已探知的但未搜尋過點”可以通過一個按f值升序的隊列(即優先隊列)進行排列。
這樣,在整體的搜尋過程中,只要按照類似廣度優先的演算法架構,從優先隊列中彈出隊首元素(f值),對其可能子結點計算g、h和f值,直到優先隊列為空白(無解)或找到終止點為止。
A*演算法與廣度優先和深度優先的聯絡就在於,當g(n)=0時,該演算法類似於DFS,當h(n)=0時,該演算法類似於BFS , 這一點,可以通過上面的A*搜尋樹的具體過程中將h(n)設為0或將g(n)設為0而得到。
A*演算法流程:
首先將起始結點S放入OPEN表,CLOSE表置空,演算法開始時:
1、如果OPEN表不為空白,從表頭取一個結點n,如果為空白演算法失敗。
2、n是目標解嗎?是,找到一個解(繼續尋找,或終止演算法)。
3、將n的所有後繼結點展開,就是從n可以直接關聯的結點(子結點),如果不在CLOSE表中,就將它們放入OPEN表,並把S放入CLOSE表,同時計算每一個後繼結點的估價值f(n),將OPEN表按f(x)排序,最小的放在表頭,重複演算法,回到1。
//OPEN-->CLOSE,起點-->任意頂點g(n)-->目標頂點h(n)
closedset := the empty set //已經被估算的節點集合
openset := set containing the initial node //將要被估算的節點集合
g_score[start] := 0 //g(n)
h_score[start] := heuristic_estimate_of_distance(start, goal) //h(n)
f_score[start] := h_score[start]
while openset is not empty //若OPEN表不為空白
x := the node in openset having the lowest f_score[] value //x為OPEN表中最小的
if x = goal //如果x是一個解
return reconstruct_path(came_from,goal) //
remove x from openset
add x to closedset //x放入CLSOE表
for each y in neighbor_nodes(x)
if y in closedset
continue
tentative_g_score := g_score[x] + dist_between(x,y)
if y not in openset
add y to openset
tentative_is_better := true
else if tentative_g_score < g_score[y]
tentative_is_better := true
else
tentative_is_better := false
if tentative_is_better = true
came_from[y] := x
g_score[y] := tentative_g_score
h_score[y] := heuristic_estimate_of_distance(y, goal) //x-->y-->goal
f_score[y] := g_score[y] + h_score[y]
return failure
function reconstruct_path(came_from,current_node)
if came_from[current_node] is set
p = reconstruct_path(came_from,came_from[current_node])
return (p + current_node)
else
return the empty path
與結點寫在一起的數值表示那個結點的價值f(n),當OPEN表為空白時CLOSE表中將求得從V0到其它所有結點的最短路徑。
考慮到演算法效能,外迴圈中每次從OPEN表取一個元素,共取了n次(共n個結點),每次展開一個結點的後續結點時,需O(n)次,同時再對OPEN表做一次排序,OPEN表大小是O(n)量級的,若用快排就是O(nlogn),乘以外迴圈總的複雜度是O(n^2 * logn),
如果每次不是對OPEN表進行排序,因為總是不斷地有新的結點添加進來,所以不用進行排序,而是每次從OPEN表中求一個最小的,那隻需要O(n)的複雜度,所以總的複雜度為O(n*n),這相當於Dijkstra演算法。
在這個演算法基礎之上稍加改進就是Dijkstra演算法。OPEN表中常出現這樣的表項:(Vk,fk1)(Vk,fk2)(Vk,fk3),而從演算法上看,只有fk最小的一個才有用,於是可以將它們合并,整個OPEN表表示當前的從V0到其它各點的最短路徑,定長為n,且初始時為V0可直接到達的權值(不能到達為INFINITY),於是就成了Dijkstra演算法。
本文完。
July、二零一一年二月十日更新。
------------------------------------------------
後續:July、二零一一年三月一日更新。
簡述A*最短路徑演算法的方法:
目標:從當前位置A到目標位置B找到一條最短的行走路徑。
方法:從A點開始,遍曆所有的可走路徑,記錄到一個結構中,記錄內容為(位置點,最小步數)
當任何第二次走到一個點的時候,判斷最小步驟是否小於記錄的內容,如果是,則更新掉原最小步數,一直到所有的路徑點都不能繼續都了為止,最終那個點被標註的最小步數既是最短路徑,
而反向找跟它相連的步數相繼少一個值的點連起來就形成了最短路徑,當多個點相同,則任意取一條即可。
總結:
A*演算法實際是個窮舉演算法,也與課本上教的最短路徑演算法類似。課本上教的是兩頭往中間走,也是所有路徑都走一次,每一個點標註最短值。
作者聲明:
本人July對本部落格所有任何內容和資料享有著作權,轉載請註明作者本人July及出處。
永遠,向您的厚道致敬。謝謝。July、二零一零年十二月二十三日。