A*最短路径算法的总结与心得——delphi实现
笔记不能贴图,一天大遗憾!原理在这里讲不清楚了,好在网上有很多这多原理解讲,参看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值来做完成。程序代码大同小异。
-----------------------
结束语:如果看完你还写不出,说明我表达力太差了,所以千万别跟我要源码!!!