AI中的幾種搜索算法---A*搜索算法
引言
A*搜索算法作爲一種典型的啓發式搜索(Informed Search)算法,這種算法與一般的算法相比,便是其在搜索過程中,會利用一些引導機制,來引導整個搜索。相比於盲目的搜索,其性能是不言而喻的。
而運用A*最廣泛的地方便是遊戲中的路徑搜索(Path Finding)。這篇文章主要會基本地介紹A*算法,並會介紹一個遊戲路徑搜索的例子。
這裏套着AI這個帽子,我會先介紹一些基本的搜索算法;隨後我會寫幾篇文章介紹AI中幾個經典概念:Machine Learning、基因算法等。
一、A*搜索算法的基本介紹
1.啓發式搜索
啓發式搜索,簡單地說,便是在搜索下一個結點的時候,利用啓發函數,能夠找到搜索代價最小的結點,進行搜索。
比較有名的啓發式搜索:BFS(Best First Search)、A * Search、SA、TABU search等等。
我這裏只介紹A*,因爲這個算法是其中名氣最大的,也是比較典型的。其中BFS和A*很類似,所以不會單獨寫一篇文章進行介紹,不過我會在之後幾種搜索算法綜合比較的時候對其進行簡單介紹;SA算法是我下篇討論的對象。
2.核心等式
我將這個等式稱爲消耗公式,意思是說搜索的結點需要耗費的“資源”。
首先簡單介紹一下這個等式:f(n) = g(n) + h(n)(公式1)。
其中g(n)表示消耗(cost),也可以這麼理解g:從出發到現在的消耗,該值是從搜索空間中計算得到。
其中h表示引導函數(heuristic function),可以這麼理解h:從現在到目標的距離,注意這裏的距離是一個抽象概念,該值是一個預測值。
3.兩個列表
然後介紹一下算法當中需要用到的兩個列表:open list和closed list。
Open list是一個優先隊列,爲每一次搜索提供最好的結點。該隊列以結點的f爲升序,該隊列是A*算法更有效率地找到從開始到目標最短的路徑。
Closed list是一個普通隊列,保存了已訪問過的結點,避免一個結點的重複訪問。
4.A*算法的流程
大致描述一下這個流程:
1. 首先便是初始化工作,初始化上面提到的兩個列表(Open list和Closed list),並獲取一個開始結點放入Open list中。
2. 然後進行具體的搜索,從優先隊列中取出最好的搜索結點(記爲cur_node),這裏最好的搜索結點便是根據公式1計算出來的,擁有最小f的結點。由於open list是一個優先隊列,所以能夠很方便地取出最優結點。
3. 如果取出的cur_node就是目標,就是我們要的結果,那麼就結束算法。
4. 將cur_node保存在closed list中。表示我們已經訪問這個結點。
5. 展開cur_node所有的鄰接結點,並進行遍歷(adj_node)。
6. 如果adj_node 已存在於closed list。表示我們已經訪問過adj_node。取下一個鄰接結點,繼續步驟6。
7. 如果結點已存在於open list中,並且cur_node比open list上的那個結點好,也就是所cur_node的g小於那個結點,那麼計算一下cur_node的f,並進行替換。
8. 如果cur_node不存在於open list中,那麼計算cur_node的f並插入open list。
下面便是一個僞代碼的流程:
Initialize the Open list.
Initialize the Closed list.
Add the start node to the open list.
loop util the open list is empty
get the best node(cur_node) from open list.
ifcur_node is the goal node
end the algorithm.
end if
add cur_node to closed list.
get the adjacent nodes(adj_node) of cur_node.
if adj_node is on the closed list
continue.
end if
if adj_node is on the open list(openlist.adjnode)
if adj_node is better than openlist.adjnode
replace the openlist.adjnode with adj_node.
end if
else
add adj_node to open list.
end else
end
end loop
這個流程可以簡單的理解:搜索的時候不停地往兩個list中插數據,open list保證每次訪問的結點是其中最好的結點,而closed list則保證要訪問的結點在之前沒有被訪問過的。
二、遊戲中的路徑搜索
這裏舉一個2D 迷宮尋出路的例子。
1.消耗公式
我們來簡要的分析一下。我們就利用公式1來進行切入,首先我們要計算出h。
當前距離目標的“距離”,這個距離可以是當前位置與目標位置之間具體的物理距離()。也可以是一個抽象距離:比如說當前位置要去目標位置的消耗,你明白去兩個地方的消耗,不光要考慮要距離,還更要考慮路況的。
當然爲了討論和實現的簡單,我們只需考慮物理距離就行了。
既然我們已經得出h,那麼接下來就考慮g,於h相類似,g的計算簡單多了,已走路線的長度就行了。
這樣整個具體的消耗公式便是:
(公式2)
2.代碼
上面說了這麼多,這裏就放一段核心的代碼,其中pqueue4path和queue4path這兩個是我自己專門爲這個例子寫的,其中pqueue4path便是那個優先隊列。
至於其中的CaculateCost,是計算結點的消耗,包括g、h和f。
PerturbPath這個函數,主要充當獲得鄰接結點的作用。
如果要看整個項目代碼請於A*演示程序於此處下載下載。
int astart_pathsearch(constPos & posStart,const Pos & posEnd,int * map,int nWidth,int nHeight,Path & solution)
{
Path path,path2;
path.length = 1;
path.positions = newPos[path.length];
path.positions[0].x = posStart.x;
path.positions[0].y = posStart.y;
CaculateCost(path,posEnd);
//
pqueue4path openlist;
queue4path closelist;
openlist.enQueue(path);
for(int depth = 1;!openlist.isEmpty();++depth)
{
openlist.deQueue(path);
if(!path.costtoend)//find the solution
{
solution.length = path.length;
solution.positions = new Pos[path.length];
for(int i = 0 ; i < path.length;++i)
{
solution.positions[i].x =path.positions[i].x;
solution.positions[i].y =path.positions[i].y;
}
delete[]path.positions;
delete[]path2.positions;
returndepth;
}
closelist.enQueue(path);
for (int i = 1 ; i < 5 ; ++i)
{
if(!perturbPath(path,map,nWidth,nHeight,i,path2))
continue;
CaculateCost(path2,posEnd);
path2.cost += depth*depth;
if(closelist.isOnList(path2))
continue;
if(openlist.isOnList(path2))
{
if(path2.cost< path.cost)
{
openlist.deletePath(path2);
openlist.enQueue(path2);
}
}
else
{
openlist.enQueue(path2);
}
}
}
delete[]path.positions;
delete[]path2.positions;
return -1;
}
3.尋路效果圖
三、總結
這是我第一次寫關於算法的文章,難免會有一些錯誤和不恰當的地方,這裏就請讀者多多包涵。
這篇文章中提到的一些代碼,可以去我的CSDN資源那邊下載,我專門寫了一個迷宮尋路的小程序。至於這個程序,由於寫比較急,所以也沒有好好的測試。如果有什麼bug或者算法有寫錯的地方,請好心的讀者指出。我也能對此進行改進,並寫出更有益的文章。
寫這篇文章主要受一位大牛博主的啓發http://blog.csdn.net/v_JULY_v。這是他的博客。
如果有興趣的可以留言,一起交流一下算法學習的心得。
接下來我會發表介紹幾種搜索算法。
聲明:本文章是筆者整理資料所得原創文章,如轉載需註明出處,謝謝。