一、前言 在這里我將對A*算法的實際應用進行一定的探討,并且舉一個有關A*算法在最短路徑搜索的例子。值得注意的是這里并不對A*的基本的概念作介紹,如果你還對A*算法不清楚的話,請看姊妹篇《初識A*算法》。 這里所舉的例子是參考AMIT主頁中的一個源程序,使用這個源程序時,應該遵守一定的公約。 二、A*算法的程序編寫原理 我在《初識A*算法》中說過,A*算法是最好優先算法的一種。只是有一些約束條件而已。我們先來看看最好優先算法是如何編寫的吧。 如圖有如下的狀態空間:(起始位置是A,目標位置是P,字母后的數字表示節點的估價值) 搜索過程中設置兩個表:OPEN和CLOSED。OPEN表保存了所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點。算法中有一步是根據估價函數重排OPEN表。這樣循環中的每一步只考慮OPEN表中狀態最好的節點。具體搜索過程如下: 1)初始狀態: OPEN=[A5];CLOSED=[]; 2)估算A5,取得搜有子節點,并放入OPEN表中; OPEN=[B4,C4,D6];CLOSED=[A5] 3)估算B4,取得搜有子節點,并放入OPEN表中; OPEN=[C4,E5,F5,D6];CLOSED=[B4,A5] 4)估算C4;取得搜有子節點,并放入OPEN表中; OPEN=[H3,G4,E5,F5,D6];CLOSED=[C4,B4,A5] 5)估算H3,取得搜有子節點,并放入OPEN表中; OPEN=[O2,P3,G4,E5,F5,D6];CLOSED=[H3,C4,B4,A5] 6)估算O2,取得搜有子節點,并放入OPEN表中; OPEN=[P3,G4,E5,F5,D6];CLOSED=[O2,H3,C4,B4,A5] 7)估算P3,已得到解; 看了具體的過程,再看看偽程序吧。算法的偽程序如下: Best_First_Search() { Open = [起始節點]; Closed = []; while (Open表非空) { 從Open中取得一個節點X,并從OPEN表中刪除。 if (X是目標節點) { 求得路徑PATH; 返回路徑PATH; } for (每一個X的子節點Y) { if (Y不在OPEN表和CLOSE表中) { 求Y的估價值; 并將Y插入OPEN表中; } //還沒有排序 else if (Y在OPEN表中) { if (Y的估價值小于OPEN表的估價值) 更新OPEN表中的估價值; } else //Y在CLOSE表中 { if (Y的估價值小于CLOSE表的估價值) { 更新CLOSE表中的估價值; 從CLOSE表中移出節點,并放入OPEN表中; } } 將X節點插入CLOSE表中; 按照估價值將OPEN表中的節點排序; }//end for }//end while }//end func 啊!偽程序出來了,寫一個源程序應該不是問題了,依葫蘆畫瓢就可以。A*算法的程序與此是一樣的,只要注意估價函數中的g(n)的h(n)約束條件就可以了。不清楚的可以看看《初識A*算法》。好了,我們可以進入另一個重要的話題,用A*算法實現最短路徑的搜索。在此之前你最好認真的理解前面的算法。不清楚可以找我。我的Email在文章尾。 三、用A*算法實現最短路徑的搜索 在游戲設計中,經常要涉及到最短路徑的搜索,現在一個比較好的方法就是用A*算法進行設計。他的好處我們就不用管了,反正就是好!^_* 注意下面所說的都是以ClassAstar這個程序為藍本,你可以在這里下載這個程序。這個程序是一個完整的工程。里面帶了一個EXE文件。可以先看看。 先復習一下,A*算法的核心是估價函數f(n),它包括g(n)和h(n)兩部分。g(n)是已經走過的代價,h(n)是n到目標的估計代價。在這個例子中g(n)表示在狀態空間從起始節點到n節點的深度,h(n)表示n節點所在地圖的位置到目標位置的直線距離。啊!一個是狀態空間,一個是實際的地圖,不要搞錯了。再詳細點說,有一個物體A,在地圖上的坐標是(xa,ya),A所要到達的目標b的坐標是(xb,yb)。則開始搜索時,設置一個起始節點1,生成八個子節點2- 9 因為有八個方向。如圖: 仔細看看節點1、9、17的g(n)和h(n)是怎么計算的。現在應該知道了下面程序中的f(n)是如何計算的吧。開始講解源程序了。其實這個程序是一個很典型的教科書似的程序,也就是說只要你看懂了上面的偽程序,這個程序是十分容易理解的。不過他和上面的偽程序有一些的不同,我在后面會提出來。 先看搜索主函數: void AstarPathfinder::FindPath(int sx, int sy, int dx, int dy)
{NODE *Node, *BestNode;int TileNumDest;//得到目標位置,作判斷用TileNumDest = TileNum(sx, sy);//生成Open和Closed表OPEN = ( NODE* )calloc(1,sizeof( NODE ));CLOSED=( NODE* )calloc(1,sizeof( NODE ));//生成起始節點,并放入Open表中Node=( NODE* )calloc(1,sizeof( NODE ));Node->g = 0;//這是計算h值// should really use sqrt().Node->h = (dx-sx)*(dx-sx) + (dy-sy)*(dy-sy);//這是計算f值,即估價值Node->f = Node->g+Node->h;Node->NodeNum = TileNum(dx, dy);Node->x = dx; Node->y = dy;// make Open List point to first nodeOPEN->NextNode=Node;for (;;){//從Open表中取得一個估價值最好的節點BestNode=ReturnBestNode();//如果該節點是目標節點就退出// if we've found the end, break and finish break;if (BestNode->NodeNum == TileNumDest)//否則生成子節點GenerateSuccessors(BestNode,sx,sy);}PATH = BestNode;
} 再看看生成子節點函數: void AstarPathfinder::GenerateSuccessors(NODE *BestNode, int dx, int dy)
{int x, y;//哦!依次生成八個方向的子節點,簡單!// Upper-Leftif ( FreeTile(x=BestNode->x-TILESIZE, y=BestNode->y-TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Upperif ( FreeTile(x=BestNode->x, y=BestNode->y-TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Upper-Rightif ( FreeTile(x=BestNode->x+TILESIZE, y=BestNode->y-TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Rightif ( FreeTile(x=BestNode->x+TILESIZE, y=BestNode->y) )GenerateSucc(BestNode,x,y,dx,dy);// Lower-Rightif ( FreeTile(x=BestNode->x+TILESIZE, y=BestNode->y+TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Lowerif ( FreeTile(x=BestNode->x, y=BestNode->y+TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Lower-Leftif ( FreeTile(x=BestNode->x-TILESIZE, y=BestNode->y+TILESIZE) )GenerateSucc(BestNode,x,y,dx,dy);// Leftif ( FreeTile(x=BestNode->x-TILESIZE, y=BestNode->y) )GenerateSucc(BestNode,x,y,dx,dy);
} 看看最重要的函數: void AstarPathfinder::GenerateSucc(NODE *BestNode,int x, int y, int dx, int dy)
{int g, TileNumS, c = 0;NODE *Old, *Successor;//計算子節點的 g 值// g(Successor)=g(BestNode)+cost of getting from BestNode to Successorg = BestNode->g+1;// identification purposesTileNumS = TileNum(x,y);//子節點再Open表中嗎?// if equal to NULL then not in OPEN list, else it returns the Node in Oldif ( (Old=CheckOPEN(TileNumS)) != NULL ){//若在for( c = 0; c < 8; c++)// Add Old to the list of BestNode's Children (or Successors).if( BestNode->Child[c] == NULL )break;BestNode->Child[c] = Old;//比較Open表中的估價值和當前的估價值(只要比較g值就可以了)// if our new g value is < Old's then reset Old's parent to point to BestNodeif ( g < Old->g ){//當前的估價值小就更新Open表中的估價值Old->Parent = BestNode;Old->g = g;Old->f = g + Old->h;}}else//在Closed表中嗎?// if equal to NULL then not in OPEN list, else it returns the Node in Oldif ( (Old=CheckCLOSED(TileNumS)) != NULL ){//若在for( c = 0; c< 8; c++)// Add Old to the list of BestNode's Children (or Successors).if ( BestNode->Child[c] == NULL )break;BestNode->Child[c] = Old;//比較Closed表中的估價值和當前的估價值(只要比較g值就可以了)// if our new g value is < Old's then reset Old's parent to point to BestNodeif ( g < Old->g ){//當前的估價值小就更新Closed表中的估價值Old->Parent = BestNode;Old->g = g;Old->f = g + Old->h;//再依次更新Old的所有子節點的估價值// Since we changed the g value of Old, we need// to propagate this new value downwards, i.e.// do a Depth-First traversal of the tree!PropagateDown(Old);}}//不在Open表中也不在Close表中else{//生成新的節點Successor = ( NODE* )calloc(1,sizeof( NODE ));Successor->Parent = BestNode;Successor->g = g;// should do sqrt(), but since we don't reallySuccessor->h = (x-dx)*(x-dx) + (y-dy)*(y-dy);// care about the distance but just which branch looksSuccessor->f = g+Successor->h;// better this should suffice. Anyayz it's faster.Successor->x = x;Successor->y = y;Successor->NodeNum = TileNumS;//再插入Open表中,同時排序。// Insert Successor on OPEN list wrt fInsert(Successor);for( c =0; c < 8; c++)// Add Old to the list of BestNode's Children (or Successors).if ( BestNode->Child[c] == NULL )break;BestNode->Child[c] = Successor;}
} 哈哈!A*算法我懂了!當然,我希望你有這樣的感覺!不過我還要再說幾句。仔細看看這個程序,你會發現,這個程序和我前面說的偽程序有一些不同,在GenerateSucc函數中,當子節點在Closed表中時,沒有將子節點從Closed表中刪除并放入Open表中。而是直接的重新的計算該節點的所有子節點的估價值(用PropagateDown函數)。這樣可以快一些!另當子節點在Open表和Closed表中時,重新的計算估價值后,沒有重新的對Open表中的節點排序,我有些想不通,為什么不排呢?:-(,會不會是一個小小的BUG。你知道告訴我好嗎? 好了!主要的內容都講完了,還是完整仔細的看看源程序吧!希望我所的對你有一點幫助,一點點也可以。如果你對文章中的觀點有異議或有更好的解釋都告訴我。我的email在文章最后! |