筆記不能貼圖,一天大遺憾!原理在這里講不清楚了,好在網上有很多這多原理解講,參看Patrick Lester先生的圖文並茂的講解,一定讓你大開眼界,只是看完還是寫不出好的源碼。我是跟據Patrick Lester先生的C++源碼改編過來的。其中有一段改的非常晦澀,完全是流氓改法,見諒!
看完Patrick Lester的文章和他的源碼(C++)后,總算知道了如何實現最有名氣最短路徑算法--A*算法了。並跟據他的提示結合廣度優先搜索法,寫出現在很流行游戲《連連看》的路徑查找方法。以下是我寫這段程序時的心得體會。
第一:數字化你的運動方向
你要求的東東是幾個運動方向:8個?4個?
Patrick Lester先生源碼是8個,《連連看》是4個。下面看看4個和8個方向是如何用循環來實現的:
8個(Patrick Lester源碼)
parentXval 起點X坐標
parentYval 起點Y坐標
MoveX 按方向移動后X坐標
MoveY 按方向移動后Y坐標
for MoveY := parentYval-1 to parentYval+1 do begin
for MoveX := parentXval-1 to parentXval+1 do begin
……
end;
end;
4個
這里有一個很好的變通方法,設置一個常量數組,因為每次只能移動一個格,把每個方向的移動的坐標變動量存入數組,用一個循環就解決了。
const
Move: array [1..4, 1..2] of Integer = ((-1, 0), (0, 1), (1, 0), (0, -1));
for i:=1 to 4 do begin
MoveX:=parentXval+Move[i,1];
MoveY:=parentYval+Move[i,2];
……
end;
因此用這種方法同樣可以解決8個方向(任何方向,如12個)的問題。只要你設置正確的MOVE數組。
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
for i:=1 to 8 do begin
MoveX:=parentXval+Move[i,1];
MoveY:=parentYval+Move[i,2];
……
end;
這是第一步,廣度搜索是要查每個節點下一步可能走的所有點。用這種方法就可以實現一次查完所有的下一步節點。
第二:節點與廣度搜索
跟據A*算法的要求,我們設置下在的一個類型,
TNode = record //定義節點類型
x, y: Integer;
Father: Integer; //指向父節點(即我是從哪個一節點運動過來的)
end;
剛開始時,想用鏈表來完成這項工作,但其實不用那么麻煩。用它也相當一個鏈表。
我們再定義一個數組。
List: array [1..MapWidth*MapHeight] of TNode;
類型中的Father 值就是List[n]的n。這么說不是很清楚。舉例說明一下。
+-------------+-----------+-----------+-----------+
| | | | |
| | 5 | 8 | |
| | | | |
| | (1,0) | (3,0) | |
+-------------+-----------+-----------+-----------+
| | 起始點 | | |
| 2 | 1 | 4 | 7 |
| | | | |
| (0,1) | (1,1) | (2,1) | (3,1) |
+-------------+-----------+-----------+-----------+
| | | | |
| | 3 | 6 | |
| | | | |
| | (1,2) | (2,2) | |
+-------------+-----------+-----------+-----------+
設置我們的起始在(1,1),首先我們知道起始點是沒有父節點的他是老大。所以
LIST[1].X:=1;
LIST[1].Y:=1;
LIST[1].Father:=0;
起始點向4個方向移動(為了省事,8方向一樣),那么就分別產生LIST[2]至LIST[5],而他們的 Father 值都是 1 ,是從起點1展出來的
我們再從(2,1)點(即List[4]這個節點)運動一下。這時就要注意了。因為(2,1)點是從(1,1)過來的,沒有必要再回去,所以它的運動只有三個點了,分別從List[6]至List[8]。他們的 Father 的值 4,是從 4 這個點展出來的
好,如果我們就是要走到(3,0)點,那么,List[8]就是結果。
我們查一下8點是如何過的:
他的father = 4,從List[4]過來的,坐標是:(2,1),
List[4]的father = 1,從List[1]過來的,坐標是:(1,1);
List[1]是從哪來的,它的Father是0,他是老大,是起點!!!
那么反過,從(1,1),(2,1),(3,0)就是剛才說的路徑。
說了一大堆,無非是要說明定義的節點類型和這個數組是如何運作的。不了解這個過程,A*就是想再多也寫不出來(至少原理上要如此)。
第三:A*理論
上面說的其實就是廣度搜索的原理。從起始點開始一直查,直到查到終點。是最費用不討好的事。A*算法是在此基礎上加上一些判斷(循環、判斷begin…end 多的嚇人,程序讀起來也非常費腦,所以本人將程序分開一步步讀解,以防哪天腦子不好使,還能寫出個A*來),智能簡化搜索過程。
說到智能,就是說要從1變4,4變16的過程中找出哪些點是好點,可能最快的到達目標。A*算法就提出這樣的理論:
F = G + H
這里:
G = 從起點A,沿着產生的路徑,移動到網格上指定方格的移動耗費。
H = 從網格上那個方格移動到終點B的預估移動耗費。這經常被稱為啟發式的,可能會讓你有點迷惑。這樣叫的原因是因為它只是個猜測。我們沒辦法事先知道路徑的長度,因為路上可能存在各種障礙(牆,水,等等)。
出現這個公式時,我們就得用8個方向來說明問題了。
G值設定。水平和豎直方向我們設為10,斜方向是水平豎直方向的1.414倍,現實生活也是如此:根號2倍的距離(公式的"耗費"我們這里用"距離"來表示,人家的東東是具有通性的,我們是辦實事)。斜方向我們取整數14,因為這樣電腦計算速度快,算出A點運動到周邊8個點的G值,如下:
| |
14 | 10 | 14
------+------+------
| A | B
10 | | 10
------+------+------
| | C
14 | 10 | 14
這些值的好處是什么呢,看上圖,從A 到C點我看一眼就能看出來,直接走斜線最近。電腦不會,它要算:A->C G值是14,A->B->C呢,A->B是10,B->C是10,加起來:20,20>14,所以要走A->C的路。
H值好理解,最簡單的方法就是:此點橫着到終點坐要走幾步,豎着到終點坐標走幾步,加起來。因為G值取值為10倍,因而下面我們的H值也將×10,這也是最簡單的一種計算方法。
注:G值的計算方法,H值的設定是非常重要的,通常是一個最短路徑能否找到的關鍵。
第四、A*的實際操作
A*算法是在廣度搜索的基礎上加了一個權(F),跟據這個權(F)再來決定,我們優選查找哪些節點(在有路徑的情況下。A*比廣度搜索快很多,在沒路的情況是一樣的,都要搜完所有可能的節點)。
思路:將起點1變為8,計算8點的F值 = G值 + H值,如下圖(7行,5列,終點右下角,無法貼圖,只能這樣說了。一定要畫出圖來才能知道H值!!):
起點周邊8點各值:
列 1 2 3 4 5
行
1 114=14+100 100=10+90 84=14+80
2 100=10+90 起點 80=10+70
3 94=14+80 80=10+70 64=14+60
……
F最低值是:64 (命名為Next1),展開右下角節點(起點不算在其內):
列 2 3 4 5
行
2 起點 144=64+10+70
*
3 144=64+10+70 Next1 124=64+10+50
* 64
4 138=64+14+60 124=64+10+50 118=64+14+40
……
注意帶"*"號的格,它們從起點和Next1點都能到達,但有時會出現不同G值,對這樣情況程序必須要處理(源碼中詳述)。
展開右下最低F值節點118 (Next2)
列 3 4 5
行
3 Next1 60+118 54+118
*
4 60+118 Next2 40+118
* 118
5 54+118 40+118 34+118
展開右下F值最低152節點(Next3):
列 4 5
行
4 Next2 40+152
*
5 40+152 Next3
* 152
6 34+152 20+152
7 終點
(6,5)點值最小,定為Next4,展開Next4點,終點就在它的展開節點中了,那就是路徑找到的充分必要條件:終點在展開節點中。於是我們就完成了這次最短路徑工作,看看是不是最短徑?!
(哈,寫和這么辛苦,想看明白還得用格表一個個去填表吧,那樣才直觀)
第五步:代碼的實現
總結上面東東:
注意點:
1、以上所上沒有障礙物,沒提到邊界;
2、帶"*"不同G值的情況沒有看到;
A*算法必須有:
1、一個打開的列表,保存了打開節點的F值,
2、每次從中取最小F值的節點打開下批子節點;
3、一個關閉列表,將已展開的節點加入其中(Next1~Next4,不包括終點)。
Patrick Lester先生的偽代碼:
1.把起始格添加到開啟列表。
2.重復如下的工作:
a) 尋找開啟列表中F值最低的格子。我們稱它為當前格。
b) 把它切換到關閉列表。
c) 對相鄰的8格中的每一個?
o如果它不可通過或者已經在關閉列表中,略過它。反之如下。
o如果它不在開啟列表中,把它添加進去。把當前節點作為這一格的父節點。記錄這一格的F,G,和H值。
o如果它已經在開啟列表中,用G值為參考檢查新的路徑是否更好。更低的G值意味着更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之后你可能需要重新對開啟列表排序。
d) 停止,當你
o把目標格添加進了開啟列表,這時候路徑被找到,或者
o沒有找到目標格,開啟列表已經空了。這時候,路徑不存在。
3.保存路徑。從目標格開始,沿着每一格的父節點移動直到回到起始格。這就是你的路徑。
偽代碼結束。
先不管它,慢慢完善代碼,直到最后結果出來。
A*算法一步一步實現:
第一步:完成將起始點向8個方向運動的過程:
定義8個運動方向
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
定義一個過程,傳入參數:起始點坐標和終點坐標
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
I:integer;
ParentXval, ParentYval:integer; //當前節點X,Y坐標
MoveX,MoveY:integer; //打開節點X,Y坐標
Begin
ParentXval:= startingX;
ParentYval:= startingY;
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
end;
end;
第二步:為保存節點做准備,我們需要一個數據結構,一個打開、關閉列表,和F、G、H值列表,同時完善地圖數據變量;
const
MapWidth = 20; //地圖寬
MapHeight = 10; //地圖高
Mapdate:array[0..MapWidth-1,0..MapHeight-1] of integer; //地圖障礙數據0可以通過,1不可以通過,一定要正確初始化此數據。
TNode = record //定義節點數據結構
x, y: Integer;
Father: Integer; //指向父節點
end;
List: array [1..MapWidth*MapHeight] of TNode; //上面有說明了
OpenList:array [1..MapWidth*MapHeight] of Integer; //這是打開節點列表,保存節點的ID號,它一個堆(注:非堆棧),一個有序堆。
whichList:array[0..MapWidth-1,0..MapHeight-1] of Integer; //關閉或者是打開的節點,用不同值來表示,我們用個常量:onClosedList=10 表示關閉。用個變量:onOpenList := onClosedList-1;表示打開。
Fcost:array[0..mapWidth*mapHeight-1] of integer;//各節點F值,注意不是坐標點!!
Gcost:array[0..mapWidth-1,0..mapHeight-1] of integer;//各坐標點G值
Hcost:array[0..mapWidth*mapHeight-1] of integer; //各節點F值,注意不是坐標點!!
好,現在將它們加入程序並初始化它們。
在展開節點前,我們要做的事:
1、whichlist數組清零;
2、初始化List[1]的坐標值,father = 0;
3、定義一個變量記錄展開節點的ID號,我們定義為:NewOpenListItemID,初始值為1;
4、初始化OpenList[1]將起始點加入其中,即OpenList[1] = 1;
5、定義變量記錄OpenList中有多少個展開節點,我們定義為:numberOfOpenListItems,初始值為1;
6、初始化onOpenList := onClosedList-1;
每展開一個節點,我們要做的事:
1、計算出節點坐標:MoveX,MoveY;
2、展開節點的ID號+1;NewOpenListItemID:= NewOpenListItemID + 1;
3、保存這個節點到List中,它有father值是OpenList[1];
4、OpenList增加了一個節點ID,OpenList[numberOfOpenListItems+1]:= NewOpenListItemID;同時numberOfOpenListItems:= numberOfOpenListItems+1;
5、計算G、H、F值;
6、將展開節點加入whichlist數組中,標記為打開:whichlist[MoveX,MoveY]:= onOpenList;
增加代碼如下:
type
TNode = record //定義節點數據結構
x, y: Integer;
Father: Integer; //指向父節點(即我是從哪個一節點運動過來的)
end;
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
MapWidth = 20; //地圖寬
MapHeight = 10; //地圖高
onClosedList=10;
var
List: array [1..MapWidth*MapHeight] of TNode;
Mapdate:array[0..MapWidth-1,0..MapHeight-1] of integer;
OpenList:array [1..MapWidth*MapHeight] of Integer;
whichList:array[0..MapWidth-1,0..MapHeight-1] of Integer;
Fcost:array[0..mapWidth*mapHeight-1] of integer;//各節點F值,注意不是坐標點!!
Gcost:array[0..mapWidth-1,0..mapHeight-1] of integer;//各坐標點G值
Hcost:array[0..mapWidth*mapHeight-1] of integer; //各坐標點H值
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
i,j:integer;
ParentXval, ParentYval:integer; //節點X,Y坐標
MoveX,MoveY:integer; //打開節點X,Y坐標
NewOpenListItemID:integer; //節點ID號,每個ID有唯一的ID
numberOfOpenListItems:integer; //OpenList中節點個數
m:integer;
AddedGCost:integer; //G值橫豎線和斜線方向值不一樣
OnOpenList:integer;
Begin
for i := 0 to mapWidth-1 do begin
for j := 0 to mapHeight-1 do
whichList [i,j] := 0;
end;
Gcost[startingX,startingY] := 0;
openList[1] := 1;//對應到第1個節點
List[1].x:=startingX;//第一節點初值
List[1].y:=startingY;
List[1].Father:=0;
NewOpenListItemID:=1;
ParentXval:= List[1].x;
ParentYval:= List[1].y;
OnOpenList:= onClosedList-1;
NumberOfOpenListItems:=1;
NewOpenListItemID:=1;
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
newOpenListItemID := newOpenListItemID + 1; //List新增加了一個節點
List[newOpenListItemID].x:= MoveX; //保存展開節點
List[newOpenListItemID].y:= MoveY;
List[newOpenListItemID].Father:= openList[1];
m := numberOfOpenListItems+1;//這里先這么寫,把numberOfOpenListItems+1寫在后面是有原因的
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
If i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
Else AddedGCost:=14; //斜對角的值
Gcost[MoveX,MoveY]:=Gcost[ParentXval, ParentYval]+ AddedGCost; //計算G值
Hcost[Openlist[m]] := 10*(abs(MoveX - targetX) + abs(MoveY - targetY));//計算H值
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
numberOfOpenListItems := numberOfOpenListItems+1;// OpenList增加一個節點
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end;
end;
這一步增加了很多A*必須的變量,但程序增加量並不多。注意各變量之間的關系!特別是openList值的問題。
第三步:排除不必要點:出界點、障礙點
我們加入兩個判斷,這樣點不必加入List和openlist中。
……
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
if (MoveX>=0) and (MoveX<MapWidth)
and (MoveY>=0) and (MoveY<MapHeight) then //判斷越界
if Mapdate[MoveX,MoveY]=0 then begin //不是障礙點
newOpenListItemID := newOpenListItemID + 1; //List新增加一個節點
List[newOpenListItemID].x:= MoveX; //保存展開節點
List[newOpenListItemID].y:= MoveX;
List[newOpenListItemID].Father:= openList[1];
m := numberOfOpenListItems+1;
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
If i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
Else AddedGCost:=14; //斜對角的值
Gcost[MoveX,MoveY]:=Gcost[ParentXval, ParentYval]+ AddedGCost; //計算G值
Hcost[Openlist[m]] := abs(MoveX - targetX) + abs(MoveY - targetY);//計算H值
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
numberOfOpenListItems := numberOfOpenListItems+1;// OpenList增加一個節點
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end;
end;
……
第四步:開始我們的循環從打開節點再打開下一級節點
這里有個明確要求,每次要打開Fcost值最小的節點,因此,我們必須找到最小的點。用什么方法找呢?Patrick Lester先生告訴我們:將openlist變成一個有序數組;一有變動就進行排序,憑他的經驗(人家朋友開發了《帝國時代游戲》),這種方法在大多數場合會快2~3倍,並且在長路徑上速度呈幾何級數提升(10倍以上速度)。於是上面的源碼增加為:
var
temp:integer; //排序用交換變量
……
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
……
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
//以下是增加代碼
while (m <> 1) do begin // 堆的插入
if (Fcost[openList[m]] <= Fcost[openList[m div 2]]) then begin
temp := openList[m div 2];
openList[m div 2] := openList[m];
openList[m] := temp;
m := m div 2;
end else break;
end;
……
排序結果是:openList[1]對應節點有最小F值,即List[openList[1]]節點值最小。
好了,開始主循環。我們每打開一個節點展開后,會有如下變化:openList節點會減少一個(openList有變動,所以要重排序),打開后要把已打開的節點保存在關閉節點中,如果openList中節點數為0,循環結束,我們用 repeat until作為主循環體。
程序變動如下:
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
……
ListFather:integer; //增加一變量,用來保存父節點
v,u:integer; //排序用變量(新增)
Begin
……
//ParentXval:= List[1].x; 這兩句移到循環體中去了
//ParentYval:= List[1].y;
repeat
if numberOfOpenListItems <>0 then begin // 循環條件
parentXval := List[openList[1]].x;//因為openList是降序的,所以只要取出openList[1],對
//應的F值就是最低的,當openList[1]值是初始值1時也是
//如此,那時它只有一個值
parentYval := List[openList[1]].y; //記錄此節點坐標
whichList[parentXval,parentYval] := onClosedList;//將此節點增加到closed list
ListFather:=openList[1];//保存起來,下面openList[1]要變動
numberOfOpenListItems := numberOfOpenListItems - 1;//OpenList 減少一個節點
openList[1] := openList[numberOfOpenListItems+1];
// 將最后一個節點放堆棧openList [1]的位置
v := 1;
repeat //排序堆, openList變動了,所以要重排序
u := v;
if (2*u+1) <= numberOfOpenListItems then begin
if (Fcost[openList[u]] >= Fcost[openList[2*u]]) then
v := 2*u;
if (Fcost[openList[v]] >= Fcost[openList[2*u+1]]) then
v := 2*u+1;
end else begin
if (2*u <= numberOfOpenListItems) then begin
if (Fcost[openList[u]] >= Fcost[openList[2*u]]) then
v := 2*u;
end;
end;
if (u <> v) then begin
temp := openList[u];
openList[u] := openList[v];
openList[v] := temp;
end else
break;
until (0>1);
for i:=1 to 8 do begin
……
List[newOpenListItemID].Father:= ListFather;// openList[1]已經是幾經變動了(新增)
……
end;
end else begin
break;
end;
until 0>1;
end;
第五步:考慮關閉,重復打開的節點的處理:
半閉了的節點我們不處理,至於曾經打開過的節點,我們可以看偽代碼怎么說的:
偽代碼中說:用G值為參考,檢查新的路徑是否更好。更低的G值意味着更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之后你可能需要重新對開啟列表排序。
A、首先增加關閉節和重復打開節點判斷:
……
if Mapdate[MoveX,MoveY]=0 then begin
if (whichList[MoveX,MoveY] <> onClosedList) then //不在關閉列表中(新增)
if (whichList[MoveX,MoveY] <> onOpenList) then begin//不在打開節點列表中(新增)
……
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end else begin //if (whichList[MoveX,MoveY] <> onOpenList)
end;//if (whichList[MoveX,MoveY] <> onClosedList)
end;
B、重新計算G值:
var
tempGcost:integer; //新增加一變量
……
end else begin
//以下為新增代碼
if i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
else AddedGCost:=14; //斜對角的值
tempGcost := Gcost[parentXval,parentYval] + addedGCost;
……
C、看看tempGcost是不是比原來的G值更小,如果更小,查找它在Openlist中位置,更換父節點、新F值:
……
tempGcost := Gcost[parentXval,parentYval] + addedGCost;
//增加以下代碼
if (tempGcost < Gcost[MoveX,MoveY]) then begin //如果 G 值更小
Gcost[MoveX,MoveY] := tempGcost; //G值改變
for j:=1 to numberOfOpenListItems do begin //查找節點在 open list中位置
if (List[openList[j]].x = movex) and
(List[openList[j]].y = movey) then begin
List[openList[j]].father := ListFather; //重新指定父節點
Fcost[openList[j]] := Gcost[Movex,movey] + Hcost[openList[j]];
m:=j;
break;
end;
end;
D、F值的改變,會帶來openlist的排序變化,找到它並重新排序:
……
break;
end;
end;
//新增代碼
while (m <> 1) do begin //插入堆
if (Fcost[openList[m]] < Fcost[openList[m div 2]]) then begin
temp := openList[m div 2];
openList[m div 2] := openList[m];
openList[m] := temp;
m := m div 2;
end else
break;
end;
end;
……
第六步:發現路徑,跳出主循環;沒找到路徑,給個說法:
在for I:=1 to 8 循環最后加入"終點是否在展開節點中"判斷來解決問決,為此我們最后一個全局變量:Foundpath:boolean; 來判斷,無路徑存、在找到路徑都要退出循環。
……
for i:=1 to 8 do begin
……
if (whichList[targetX,targetY] = onOpenList) then begin
Foundpath:=true;
break; //注這是跳出for 循環
end;
end;
until Foundpath;//這里就要改了不能再是0>1了
注意此時此刻的end是一大堆了。你最好在每個end后面注明這是什么的end;否則一個字:暈!
沒找到路徑的情況就是numberOfOpenListItems<>0這個條件,當openlist中都沒有東東時候,所有該查節點都查完了。加入程序中:
……
repeat
if (numberOfOpenListItems <> 0) then begin
……
end else begin
Foundpath:=false;//加入此句
Break;
end;
until foundPath;
第七步:取出路徑
到現在,路是找到了,但只有電腦知道,都在list中。我們要找它出來,首先我們必須知道,終點是第幾個節點,順藤摸瓜,找到起點,得到反序的路徑,再把它正過來。
終點是第幾個節點?
每增加一個節點(newOpenListItemID),我們都會判斷它是不是終點,如果是就跳出循環了。所以newOpenListItemID就是終點的節點數,我們用返參的形式,把它返回到一個全局變量,那么開始定義的findpath過程就變為:
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer;var Pathint:integer);
begin
……
until Foundpath;
Pathint:= newOpenListItemID;
end;
取出路徑代碼:
這里是仁人見仁,智者見智,愛怎么寫就怎么寫了。這部份代碼我沒有參照Patrick Lester先生源碼。還要說明的一點是,Patrick Lester的A*代碼一次可以算N條路徑,條件是同一終點。正如即時戰斗類游戲中,選中一大堆小兵后,去攻打某一東西。他老人家一次就算完了,這種處理方式--高,實在是高!在FindPath 中再加一個變量就可以實現,但不在我研究范圍內。
Path:array[1.. MapWidth*MapHeight,1..2]of integer;//路徑全局變量
procedure GetPathary(var Pathstep:integer);
Var
Int:integer;
N:TNode;
I:integer;
a:integer;
begin
N:= List[EndInt];//從最后一個開始讀起
Int:=0;
while N.father<>0 do begin // 一直讀到起點
Inc(Int);
Path[int,1]:=n.x;
Path[int,2]:=n.y; //得反序路徑
N:=List[n.father];
end;
Pathstep:=int; //返回路徑步數供程序使用
for i:=1 to (int div 2 )do begin //把路徑正過來
a:=Path[i,1];Path[i,1]:=Path[int,1];Path[int,1]:=a;
a:=Path[i,2];Path[i,2]:=Path[int,2];Path[int,2]:=a;
Dec(int);
end;
end;
至此8方向A*查路徑全部結束。path就是路徑坐標。
----------------------------
浮想篇:
《連連看》路徑查找方法
首先分析《連連看》,它只能4個方向移動,它要求最多只能轉折三次。因此,轉折是必須加入考慮的東東,我們以轉彎越少越好,把它加入到F值,轉彎我設它為E值,那么 F = G + H + E。
跟據上面程序改動如下:
首先是4個方向移動
const
Move: array [1..4, 1..2] of Integer = ((-1, 0), (0, 1), (1, 0), (0, -1));
Var
Ecost:array[0.. mapWidth*mapHeight-1] of integer;
Tnode 也要改變,增加兩個變量,一個它從父節點上是從哪個方向來的:X方向或Y方向;從起點到這點轉了幾次彎,也就是E值(上面8個方向的F,G,H值其實都可以放在Tnode中);
TNode = record //定義節點類型
x, y: Integer;
way:byte; // X方向或Y方向
wayint:integer; //轉了幾次彎,也就是E值
Father: Integer; //父結點指針
end;
增加一個過程,用來判斷從起點到這點轉了幾個彎:
procedure GetEcost(Index:integer;var wayint:integer);
var
N:TNode;
Way:byte;
begin
N:=List[Index];
way:=N.way;
wayint:=0;
repeat
if (N.Father<>0)and(way<>N.way) then begin
Way:=N.way;
wayint:=wayint+1;
end;
N:=List[N.father];
until N.Father=0;
wayint:=wayint+1;
end;
在增加numberOfOpenListItems時,先看看wayint是不是小於3,是加,不是就不加!即比8方向增加了一個判斷。
最后一個重復打開節點問題:
4方向和8方向不一樣,G值考慮價值不大,所以我采用的是用更好的Ecost值來做完成。程序代碼大同小異。
-----------------------
結束語:如果看完你還寫不出,說明我表達力太差了,所以千萬別跟我要源碼!!!
看完Patrick Lester的文章和他的源碼(C++)后,總算知道了如何實現最有名氣最短路徑算法--A*算法了。並跟據他的提示結合廣度優先搜索法,寫出現在很流行游戲《連連看》的路徑查找方法。以下是我寫這段程序時的心得體會。
第一:數字化你的運動方向
你要求的東東是幾個運動方向:8個?4個?
Patrick Lester先生源碼是8個,《連連看》是4個。下面看看4個和8個方向是如何用循環來實現的:
8個(Patrick Lester源碼)
parentXval 起點X坐標
parentYval 起點Y坐標
MoveX 按方向移動后X坐標
MoveY 按方向移動后Y坐標
for MoveY := parentYval-1 to parentYval+1 do begin
for MoveX := parentXval-1 to parentXval+1 do begin
……
end;
end;
4個
這里有一個很好的變通方法,設置一個常量數組,因為每次只能移動一個格,把每個方向的移動的坐標變動量存入數組,用一個循環就解決了。
const
Move: array [1..4, 1..2] of Integer = ((-1, 0), (0, 1), (1, 0), (0, -1));
for i:=1 to 4 do begin
MoveX:=parentXval+Move[i,1];
MoveY:=parentYval+Move[i,2];
……
end;
因此用這種方法同樣可以解決8個方向(任何方向,如12個)的問題。只要你設置正確的MOVE數組。
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
for i:=1 to 8 do begin
MoveX:=parentXval+Move[i,1];
MoveY:=parentYval+Move[i,2];
……
end;
這是第一步,廣度搜索是要查每個節點下一步可能走的所有點。用這種方法就可以實現一次查完所有的下一步節點。
第二:節點與廣度搜索
跟據A*算法的要求,我們設置下在的一個類型,
TNode = record //定義節點類型
x, y: Integer;
Father: Integer; //指向父節點(即我是從哪個一節點運動過來的)
end;
剛開始時,想用鏈表來完成這項工作,但其實不用那么麻煩。用它也相當一個鏈表。
我們再定義一個數組。
List: array [1..MapWidth*MapHeight] of TNode;
類型中的Father 值就是List[n]的n。這么說不是很清楚。舉例說明一下。
+-------------+-----------+-----------+-----------+
| | | | |
| | 5 | 8 | |
| | | | |
| | (1,0) | (3,0) | |
+-------------+-----------+-----------+-----------+
| | 起始點 | | |
| 2 | 1 | 4 | 7 |
| | | | |
| (0,1) | (1,1) | (2,1) | (3,1) |
+-------------+-----------+-----------+-----------+
| | | | |
| | 3 | 6 | |
| | | | |
| | (1,2) | (2,2) | |
+-------------+-----------+-----------+-----------+
設置我們的起始在(1,1),首先我們知道起始點是沒有父節點的他是老大。所以
LIST[1].X:=1;
LIST[1].Y:=1;
LIST[1].Father:=0;
起始點向4個方向移動(為了省事,8方向一樣),那么就分別產生LIST[2]至LIST[5],而他們的 Father 值都是 1 ,是從起點1展出來的
我們再從(2,1)點(即List[4]這個節點)運動一下。這時就要注意了。因為(2,1)點是從(1,1)過來的,沒有必要再回去,所以它的運動只有三個點了,分別從List[6]至List[8]。他們的 Father 的值 4,是從 4 這個點展出來的
好,如果我們就是要走到(3,0)點,那么,List[8]就是結果。
我們查一下8點是如何過的:
他的father = 4,從List[4]過來的,坐標是:(2,1),
List[4]的father = 1,從List[1]過來的,坐標是:(1,1);
List[1]是從哪來的,它的Father是0,他是老大,是起點!!!
那么反過,從(1,1),(2,1),(3,0)就是剛才說的路徑。
說了一大堆,無非是要說明定義的節點類型和這個數組是如何運作的。不了解這個過程,A*就是想再多也寫不出來(至少原理上要如此)。
第三:A*理論
上面說的其實就是廣度搜索的原理。從起始點開始一直查,直到查到終點。是最費用不討好的事。A*算法是在此基礎上加上一些判斷(循環、判斷begin…end 多的嚇人,程序讀起來也非常費腦,所以本人將程序分開一步步讀解,以防哪天腦子不好使,還能寫出個A*來),智能簡化搜索過程。
說到智能,就是說要從1變4,4變16的過程中找出哪些點是好點,可能最快的到達目標。A*算法就提出這樣的理論:
F = G + H
這里:
G = 從起點A,沿着產生的路徑,移動到網格上指定方格的移動耗費。
H = 從網格上那個方格移動到終點B的預估移動耗費。這經常被稱為啟發式的,可能會讓你有點迷惑。這樣叫的原因是因為它只是個猜測。我們沒辦法事先知道路徑的長度,因為路上可能存在各種障礙(牆,水,等等)。
出現這個公式時,我們就得用8個方向來說明問題了。
G值設定。水平和豎直方向我們設為10,斜方向是水平豎直方向的1.414倍,現實生活也是如此:根號2倍的距離(公式的"耗費"我們這里用"距離"來表示,人家的東東是具有通性的,我們是辦實事)。斜方向我們取整數14,因為這樣電腦計算速度快,算出A點運動到周邊8個點的G值,如下:
| |
14 | 10 | 14
------+------+------
| A | B
10 | | 10
------+------+------
| | C
14 | 10 | 14
這些值的好處是什么呢,看上圖,從A 到C點我看一眼就能看出來,直接走斜線最近。電腦不會,它要算:A->C G值是14,A->B->C呢,A->B是10,B->C是10,加起來:20,20>14,所以要走A->C的路。
H值好理解,最簡單的方法就是:此點橫着到終點坐要走幾步,豎着到終點坐標走幾步,加起來。因為G值取值為10倍,因而下面我們的H值也將×10,這也是最簡單的一種計算方法。
注:G值的計算方法,H值的設定是非常重要的,通常是一個最短路徑能否找到的關鍵。
第四、A*的實際操作
A*算法是在廣度搜索的基礎上加了一個權(F),跟據這個權(F)再來決定,我們優選查找哪些節點(在有路徑的情況下。A*比廣度搜索快很多,在沒路的情況是一樣的,都要搜完所有可能的節點)。
思路:將起點1變為8,計算8點的F值 = G值 + H值,如下圖(7行,5列,終點右下角,無法貼圖,只能這樣說了。一定要畫出圖來才能知道H值!!):
起點周邊8點各值:
列 1 2 3 4 5
行
1 114=14+100 100=10+90 84=14+80
2 100=10+90 起點 80=10+70
3 94=14+80 80=10+70 64=14+60
……
F最低值是:64 (命名為Next1),展開右下角節點(起點不算在其內):
列 2 3 4 5
行
2 起點 144=64+10+70
*
3 144=64+10+70 Next1 124=64+10+50
* 64
4 138=64+14+60 124=64+10+50 118=64+14+40
……
注意帶"*"號的格,它們從起點和Next1點都能到達,但有時會出現不同G值,對這樣情況程序必須要處理(源碼中詳述)。
展開右下最低F值節點118 (Next2)
列 3 4 5
行
3 Next1 60+118 54+118
*
4 60+118 Next2 40+118
* 118
5 54+118 40+118 34+118
展開右下F值最低152節點(Next3):
列 4 5
行
4 Next2 40+152
*
5 40+152 Next3
* 152
6 34+152 20+152
7 終點
(6,5)點值最小,定為Next4,展開Next4點,終點就在它的展開節點中了,那就是路徑找到的充分必要條件:終點在展開節點中。於是我們就完成了這次最短路徑工作,看看是不是最短徑?!
(哈,寫和這么辛苦,想看明白還得用格表一個個去填表吧,那樣才直觀)
第五步:代碼的實現
總結上面東東:
注意點:
1、以上所上沒有障礙物,沒提到邊界;
2、帶"*"不同G值的情況沒有看到;
A*算法必須有:
1、一個打開的列表,保存了打開節點的F值,
2、每次從中取最小F值的節點打開下批子節點;
3、一個關閉列表,將已展開的節點加入其中(Next1~Next4,不包括終點)。
Patrick Lester先生的偽代碼:
1.把起始格添加到開啟列表。
2.重復如下的工作:
a) 尋找開啟列表中F值最低的格子。我們稱它為當前格。
b) 把它切換到關閉列表。
c) 對相鄰的8格中的每一個?
o如果它不可通過或者已經在關閉列表中,略過它。反之如下。
o如果它不在開啟列表中,把它添加進去。把當前節點作為這一格的父節點。記錄這一格的F,G,和H值。
o如果它已經在開啟列表中,用G值為參考檢查新的路徑是否更好。更低的G值意味着更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之后你可能需要重新對開啟列表排序。
d) 停止,當你
o把目標格添加進了開啟列表,這時候路徑被找到,或者
o沒有找到目標格,開啟列表已經空了。這時候,路徑不存在。
3.保存路徑。從目標格開始,沿着每一格的父節點移動直到回到起始格。這就是你的路徑。
偽代碼結束。
先不管它,慢慢完善代碼,直到最后結果出來。
A*算法一步一步實現:
第一步:完成將起始點向8個方向運動的過程:
定義8個運動方向
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
定義一個過程,傳入參數:起始點坐標和終點坐標
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
I:integer;
ParentXval, ParentYval:integer; //當前節點X,Y坐標
MoveX,MoveY:integer; //打開節點X,Y坐標
Begin
ParentXval:= startingX;
ParentYval:= startingY;
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
end;
end;
第二步:為保存節點做准備,我們需要一個數據結構,一個打開、關閉列表,和F、G、H值列表,同時完善地圖數據變量;
const
MapWidth = 20; //地圖寬
MapHeight = 10; //地圖高
Mapdate:array[0..MapWidth-1,0..MapHeight-1] of integer; //地圖障礙數據0可以通過,1不可以通過,一定要正確初始化此數據。
TNode = record //定義節點數據結構
x, y: Integer;
Father: Integer; //指向父節點
end;
List: array [1..MapWidth*MapHeight] of TNode; //上面有說明了
OpenList:array [1..MapWidth*MapHeight] of Integer; //這是打開節點列表,保存節點的ID號,它一個堆(注:非堆棧),一個有序堆。
whichList:array[0..MapWidth-1,0..MapHeight-1] of Integer; //關閉或者是打開的節點,用不同值來表示,我們用個常量:onClosedList=10 表示關閉。用個變量:onOpenList := onClosedList-1;表示打開。
Fcost:array[0..mapWidth*mapHeight-1] of integer;//各節點F值,注意不是坐標點!!
Gcost:array[0..mapWidth-1,0..mapHeight-1] of integer;//各坐標點G值
Hcost:array[0..mapWidth*mapHeight-1] of integer; //各節點F值,注意不是坐標點!!
好,現在將它們加入程序並初始化它們。
在展開節點前,我們要做的事:
1、whichlist數組清零;
2、初始化List[1]的坐標值,father = 0;
3、定義一個變量記錄展開節點的ID號,我們定義為:NewOpenListItemID,初始值為1;
4、初始化OpenList[1]將起始點加入其中,即OpenList[1] = 1;
5、定義變量記錄OpenList中有多少個展開節點,我們定義為:numberOfOpenListItems,初始值為1;
6、初始化onOpenList := onClosedList-1;
每展開一個節點,我們要做的事:
1、計算出節點坐標:MoveX,MoveY;
2、展開節點的ID號+1;NewOpenListItemID:= NewOpenListItemID + 1;
3、保存這個節點到List中,它有father值是OpenList[1];
4、OpenList增加了一個節點ID,OpenList[numberOfOpenListItems+1]:= NewOpenListItemID;同時numberOfOpenListItems:= numberOfOpenListItems+1;
5、計算G、H、F值;
6、將展開節點加入whichlist數組中,標記為打開:whichlist[MoveX,MoveY]:= onOpenList;
增加代碼如下:
type
TNode = record //定義節點數據結構
x, y: Integer;
Father: Integer; //指向父節點(即我是從哪個一節點運動過來的)
end;
const
Move: array [1..8, 1..2] of Integer = ((-1, 0), (-1,1),(0, 1),(1,1), (1, 0),(-1,1), (0, -1),(-1,-1));
MapWidth = 20; //地圖寬
MapHeight = 10; //地圖高
onClosedList=10;
var
List: array [1..MapWidth*MapHeight] of TNode;
Mapdate:array[0..MapWidth-1,0..MapHeight-1] of integer;
OpenList:array [1..MapWidth*MapHeight] of Integer;
whichList:array[0..MapWidth-1,0..MapHeight-1] of Integer;
Fcost:array[0..mapWidth*mapHeight-1] of integer;//各節點F值,注意不是坐標點!!
Gcost:array[0..mapWidth-1,0..mapHeight-1] of integer;//各坐標點G值
Hcost:array[0..mapWidth*mapHeight-1] of integer; //各坐標點H值
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
i,j:integer;
ParentXval, ParentYval:integer; //節點X,Y坐標
MoveX,MoveY:integer; //打開節點X,Y坐標
NewOpenListItemID:integer; //節點ID號,每個ID有唯一的ID
numberOfOpenListItems:integer; //OpenList中節點個數
m:integer;
AddedGCost:integer; //G值橫豎線和斜線方向值不一樣
OnOpenList:integer;
Begin
for i := 0 to mapWidth-1 do begin
for j := 0 to mapHeight-1 do
whichList [i,j] := 0;
end;
Gcost[startingX,startingY] := 0;
openList[1] := 1;//對應到第1個節點
List[1].x:=startingX;//第一節點初值
List[1].y:=startingY;
List[1].Father:=0;
NewOpenListItemID:=1;
ParentXval:= List[1].x;
ParentYval:= List[1].y;
OnOpenList:= onClosedList-1;
NumberOfOpenListItems:=1;
NewOpenListItemID:=1;
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
newOpenListItemID := newOpenListItemID + 1; //List新增加了一個節點
List[newOpenListItemID].x:= MoveX; //保存展開節點
List[newOpenListItemID].y:= MoveY;
List[newOpenListItemID].Father:= openList[1];
m := numberOfOpenListItems+1;//這里先這么寫,把numberOfOpenListItems+1寫在后面是有原因的
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
If i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
Else AddedGCost:=14; //斜對角的值
Gcost[MoveX,MoveY]:=Gcost[ParentXval, ParentYval]+ AddedGCost; //計算G值
Hcost[Openlist[m]] := 10*(abs(MoveX - targetX) + abs(MoveY - targetY));//計算H值
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
numberOfOpenListItems := numberOfOpenListItems+1;// OpenList增加一個節點
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end;
end;
這一步增加了很多A*必須的變量,但程序增加量並不多。注意各變量之間的關系!特別是openList值的問題。
第三步:排除不必要點:出界點、障礙點
我們加入兩個判斷,這樣點不必加入List和openlist中。
……
for i:=1 to 8 do begin
MoveX:= ParentXval+Move[i,1];
MoveY:= ParentYval+Move[i,2];
if (MoveX>=0) and (MoveX<MapWidth)
and (MoveY>=0) and (MoveY<MapHeight) then //判斷越界
if Mapdate[MoveX,MoveY]=0 then begin //不是障礙點
newOpenListItemID := newOpenListItemID + 1; //List新增加一個節點
List[newOpenListItemID].x:= MoveX; //保存展開節點
List[newOpenListItemID].y:= MoveX;
List[newOpenListItemID].Father:= openList[1];
m := numberOfOpenListItems+1;
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
If i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
Else AddedGCost:=14; //斜對角的值
Gcost[MoveX,MoveY]:=Gcost[ParentXval, ParentYval]+ AddedGCost; //計算G值
Hcost[Openlist[m]] := abs(MoveX - targetX) + abs(MoveY - targetY);//計算H值
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
numberOfOpenListItems := numberOfOpenListItems+1;// OpenList增加一個節點
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end;
end;
……
第四步:開始我們的循環從打開節點再打開下一級節點
這里有個明確要求,每次要打開Fcost值最小的節點,因此,我們必須找到最小的點。用什么方法找呢?Patrick Lester先生告訴我們:將openlist變成一個有序數組;一有變動就進行排序,憑他的經驗(人家朋友開發了《帝國時代游戲》),這種方法在大多數場合會快2~3倍,並且在長路徑上速度呈幾何級數提升(10倍以上速度)。於是上面的源碼增加為:
var
temp:integer; //排序用交換變量
……
openList[m] := newOpenListItemID; //將新節點(ID號)放在open list最后
……
Fcost[openList[m]] := Gcost[MoveX,MoveY]+Hcost[Openlist[m]]; //得出F值
//以下是增加代碼
while (m <> 1) do begin // 堆的插入
if (Fcost[openList[m]] <= Fcost[openList[m div 2]]) then begin
temp := openList[m div 2];
openList[m div 2] := openList[m];
openList[m] := temp;
m := m div 2;
end else break;
end;
……
排序結果是:openList[1]對應節點有最小F值,即List[openList[1]]節點值最小。
好了,開始主循環。我們每打開一個節點展開后,會有如下變化:openList節點會減少一個(openList有變動,所以要重排序),打開后要把已打開的節點保存在關閉節點中,如果openList中節點數為0,循環結束,我們用 repeat until作為主循環體。
程序變動如下:
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer);
var
……
ListFather:integer; //增加一變量,用來保存父節點
v,u:integer; //排序用變量(新增)
Begin
……
//ParentXval:= List[1].x; 這兩句移到循環體中去了
//ParentYval:= List[1].y;
repeat
if numberOfOpenListItems <>0 then begin // 循環條件
parentXval := List[openList[1]].x;//因為openList是降序的,所以只要取出openList[1],對
//應的F值就是最低的,當openList[1]值是初始值1時也是
//如此,那時它只有一個值
parentYval := List[openList[1]].y; //記錄此節點坐標
whichList[parentXval,parentYval] := onClosedList;//將此節點增加到closed list
ListFather:=openList[1];//保存起來,下面openList[1]要變動
numberOfOpenListItems := numberOfOpenListItems - 1;//OpenList 減少一個節點
openList[1] := openList[numberOfOpenListItems+1];
// 將最后一個節點放堆棧openList [1]的位置
v := 1;
repeat //排序堆, openList變動了,所以要重排序
u := v;
if (2*u+1) <= numberOfOpenListItems then begin
if (Fcost[openList[u]] >= Fcost[openList[2*u]]) then
v := 2*u;
if (Fcost[openList[v]] >= Fcost[openList[2*u+1]]) then
v := 2*u+1;
end else begin
if (2*u <= numberOfOpenListItems) then begin
if (Fcost[openList[u]] >= Fcost[openList[2*u]]) then
v := 2*u;
end;
end;
if (u <> v) then begin
temp := openList[u];
openList[u] := openList[v];
openList[v] := temp;
end else
break;
until (0>1);
for i:=1 to 8 do begin
……
List[newOpenListItemID].Father:= ListFather;// openList[1]已經是幾經變動了(新增)
……
end;
end else begin
break;
end;
until 0>1;
end;
第五步:考慮關閉,重復打開的節點的處理:
半閉了的節點我們不處理,至於曾經打開過的節點,我們可以看偽代碼怎么說的:
偽代碼中說:用G值為參考,檢查新的路徑是否更好。更低的G值意味着更好的路徑。如果是這樣,就把這一格的父節點改成當前格,並且重新計算這一格的G和F值。如果你保持你的開啟列表按F值排序,改變之后你可能需要重新對開啟列表排序。
A、首先增加關閉節和重復打開節點判斷:
……
if Mapdate[MoveX,MoveY]=0 then begin
if (whichList[MoveX,MoveY] <> onClosedList) then //不在關閉列表中(新增)
if (whichList[MoveX,MoveY] <> onOpenList) then begin//不在打開節點列表中(新增)
……
whichList[MoveX,MoveY] := onOpenList; //這個節點展開了
end else begin //if (whichList[MoveX,MoveY] <> onOpenList)
end;//if (whichList[MoveX,MoveY] <> onClosedList)
end;
B、重新計算G值:
var
tempGcost:integer; //新增加一變量
……
end else begin
//以下為新增代碼
if i in [1,3,5,7] then AddedGCost:=10 //非斜對角的值
else AddedGCost:=14; //斜對角的值
tempGcost := Gcost[parentXval,parentYval] + addedGCost;
……
C、看看tempGcost是不是比原來的G值更小,如果更小,查找它在Openlist中位置,更換父節點、新F值:
……
tempGcost := Gcost[parentXval,parentYval] + addedGCost;
//增加以下代碼
if (tempGcost < Gcost[MoveX,MoveY]) then begin //如果 G 值更小
Gcost[MoveX,MoveY] := tempGcost; //G值改變
for j:=1 to numberOfOpenListItems do begin //查找節點在 open list中位置
if (List[openList[j]].x = movex) and
(List[openList[j]].y = movey) then begin
List[openList[j]].father := ListFather; //重新指定父節點
Fcost[openList[j]] := Gcost[Movex,movey] + Hcost[openList[j]];
m:=j;
break;
end;
end;
D、F值的改變,會帶來openlist的排序變化,找到它並重新排序:
……
break;
end;
end;
//新增代碼
while (m <> 1) do begin //插入堆
if (Fcost[openList[m]] < Fcost[openList[m div 2]]) then begin
temp := openList[m div 2];
openList[m div 2] := openList[m];
openList[m] := temp;
m := m div 2;
end else
break;
end;
end;
……
第六步:發現路徑,跳出主循環;沒找到路徑,給個說法:
在for I:=1 to 8 循環最后加入"終點是否在展開節點中"判斷來解決問決,為此我們最后一個全局變量:Foundpath:boolean; 來判斷,無路徑存、在找到路徑都要退出循環。
……
for i:=1 to 8 do begin
……
if (whichList[targetX,targetY] = onOpenList) then begin
Foundpath:=true;
break; //注這是跳出for 循環
end;
end;
until Foundpath;//這里就要改了不能再是0>1了
注意此時此刻的end是一大堆了。你最好在每個end后面注明這是什么的end;否則一個字:暈!
沒找到路徑的情況就是numberOfOpenListItems<>0這個條件,當openlist中都沒有東東時候,所有該查節點都查完了。加入程序中:
……
repeat
if (numberOfOpenListItems <> 0) then begin
……
end else begin
Foundpath:=false;//加入此句
Break;
end;
until foundPath;
第七步:取出路徑
到現在,路是找到了,但只有電腦知道,都在list中。我們要找它出來,首先我們必須知道,終點是第幾個節點,順藤摸瓜,找到起點,得到反序的路徑,再把它正過來。
終點是第幾個節點?
每增加一個節點(newOpenListItemID),我們都會判斷它是不是終點,如果是就跳出循環了。所以newOpenListItemID就是終點的節點數,我們用返參的形式,把它返回到一個全局變量,那么開始定義的findpath過程就變為:
procedure FindPath (startingX,startingY:integer;
targetX,targetY:integer;var Pathint:integer);
begin
……
until Foundpath;
Pathint:= newOpenListItemID;
end;
取出路徑代碼:
這里是仁人見仁,智者見智,愛怎么寫就怎么寫了。這部份代碼我沒有參照Patrick Lester先生源碼。還要說明的一點是,Patrick Lester的A*代碼一次可以算N條路徑,條件是同一終點。正如即時戰斗類游戲中,選中一大堆小兵后,去攻打某一東西。他老人家一次就算完了,這種處理方式--高,實在是高!在FindPath 中再加一個變量就可以實現,但不在我研究范圍內。
Path:array[1.. MapWidth*MapHeight,1..2]of integer;//路徑全局變量
procedure GetPathary(var Pathstep:integer);
Var
Int:integer;
N:TNode;
I:integer;
a:integer;
begin
N:= List[EndInt];//從最后一個開始讀起
Int:=0;
while N.father<>0 do begin // 一直讀到起點
Inc(Int);
Path[int,1]:=n.x;
Path[int,2]:=n.y; //得反序路徑
N:=List[n.father];
end;
Pathstep:=int; //返回路徑步數供程序使用
for i:=1 to (int div 2 )do begin //把路徑正過來
a:=Path[i,1];Path[i,1]:=Path[int,1];Path[int,1]:=a;
a:=Path[i,2];Path[i,2]:=Path[int,2];Path[int,2]:=a;
Dec(int);
end;
end;
至此8方向A*查路徑全部結束。path就是路徑坐標。
----------------------------
浮想篇:
《連連看》路徑查找方法
首先分析《連連看》,它只能4個方向移動,它要求最多只能轉折三次。因此,轉折是必須加入考慮的東東,我們以轉彎越少越好,把它加入到F值,轉彎我設它為E值,那么 F = G + H + E。
跟據上面程序改動如下:
首先是4個方向移動
const
Move: array [1..4, 1..2] of Integer = ((-1, 0), (0, 1), (1, 0), (0, -1));
Var
Ecost:array[0.. mapWidth*mapHeight-1] of integer;
Tnode 也要改變,增加兩個變量,一個它從父節點上是從哪個方向來的:X方向或Y方向;從起點到這點轉了幾次彎,也就是E值(上面8個方向的F,G,H值其實都可以放在Tnode中);
TNode = record //定義節點類型
x, y: Integer;
way:byte; // X方向或Y方向
wayint:integer; //轉了幾次彎,也就是E值
Father: Integer; //父結點指針
end;
增加一個過程,用來判斷從起點到這點轉了幾個彎:
procedure GetEcost(Index:integer;var wayint:integer);
var
N:TNode;
Way:byte;
begin
N:=List[Index];
way:=N.way;
wayint:=0;
repeat
if (N.Father<>0)and(way<>N.way) then begin
Way:=N.way;
wayint:=wayint+1;
end;
N:=List[N.father];
until N.Father=0;
wayint:=wayint+1;
end;
在增加numberOfOpenListItems時,先看看wayint是不是小於3,是加,不是就不加!即比8方向增加了一個判斷。
最后一個重復打開節點問題:
4方向和8方向不一樣,G值考慮價值不大,所以我采用的是用更好的Ecost值來做完成。程序代碼大同小異。
-----------------------
結束語:如果看完你還寫不出,說明我表達力太差了,所以千萬別跟我要源碼!!!