A*搜索算法(Java实现)

如果需要阅读代码,请移步: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*算法横空出世了,并在相关领域得到了广泛的应用。 

A* 搜索算法

    图形搜索算法,从给定起点到给定终点计算出路径。其中使用了一种启发式的估算,为每个节点估算通过该节点的最佳路径,并以之为各个地点排定次序。算法以得到的次序访问这些节点。因此,A*搜索算法是最佳优先搜索的范例。

Graph search algorithm that finds a path from a given initial node to a givengoal node. It employs a heuristic estimate that ranks each node by an estimateof the best route that goes through that node. It visits the nodes in order ofthis heuristic estimate. The A* algorithm is therefore an example of best-firstsearch.

相关概念

搜索区域(The Search Area)

我们假设某人要从 A 点移动到 B 点,但是这两点之间被一堵墙隔开。如图 1 ,绿色是 A ,红色是 B ,中间蓝色是墙。

搜索区域划分为一个矩阵

通过图片我们可以发现,我们把要搜寻的区域划分成了正方形的格子,一个矩阵。这是寻路的第一步,简化搜索区域,就像我们这里做的一样。这个特殊的方法把我们的搜索区域简化为了 2 维数组。数组的每一项代表一个格子,它的状态就是可走 (walkalbe) 和不可走 (unwalkable) 。通过计算出从 A 到 B需要走过哪些方格,就找到了路径。一旦路径找到了,人物便从一个方格的中心移动到另一个方格的中心,直至到达目的地。

方格的中心点我们成为“节点 (nodes) ”。如果你读过其他关于 A* 寻路算法的文章,你会发现人们常常都在讨论节点。为什么不直接描述为方格呢?因为我们有可能把搜索区域划为为其他多变形而不是正方形,例如可以是六边形,矩形,甚至可以是任意多变形。而节点可以放在任意多边形里面,可以放在多变形的中心,也可以放在多边形的边上。我们使用这个系统,因为它最简单。

开始搜索(Starting the Search)

    一旦我们把搜寻区域简化为一组可以量化的节点后,就像上面做的一样,我们下一步要做的便是查找最短路径。在 A* 中,我们从起点开始,检查其相邻的方格,然后向四周扩展,直至找到目标。

     我们这样开始我们的寻路旅途:

      1. 从起点 A 开始,并把它就加入到一个由方格组成的 open list( 开放列表 ) 中。这个 open list 有点像是一个购物单。当然现在 open list 里只有一项,它就是起点 A ,后面会慢慢加入更多的项。 Open list 里的格子是路径可能会是沿途经过的,也有可能不经过。基本上 open list 是一个待检查的方格列表。

      2.查看与起点 A 相邻的方格 ( 忽略其中墙壁所占领的方格,河流所占领的方格及其他非法地形占领的方格 ) ,把其中可走的 (walkable) 或可到达的 (reachable) 方格也加入到 open list 中。把起点 A 设置为这些方格的父亲 (parent node 或 parent square) 。当我们在追踪路径时,这些父节点的内容是很重要的。稍后解释       

       3. 把 A 从 open list 中移除,加入到 close list( 封闭列表 ) 中, close list 中的每个方格都是现在不需要再关注的。如下图所示,深绿色的方格为起点,它的外框是亮蓝色,表示该方格被加入到了 close list 。与它相邻的黑色方格是需要被检查的,他们的外框是亮绿色。每个黑方格都有一个灰色的指针指向他们的父节点,这里是起点 A 。


每个邻接点都有一个指向当前点的指针

下一步,我们需要从 open list 中选一个与起点 A 相邻的方格,按下面描述的一样或多或少的重复前面的步骤。但是到底选择哪个方格好呢?具有最小 F 值的那个。 

      路径排序(Path Sorting)计算出组成路径的方格的关键是下面这个等式:

        F = G + H

       这里,G = 从起点 A 移动到指定方格的移动代价,沿着到达该方格而生成的路径。

        H = 从指定的方格移动到终点 B 的估算成本。在接下来的代码中,我计算H采用的是Monhattan距离,这个距离是指忽略图中的障碍物,仅考虑横向和纵向的移动,在用而二维数组表示的方式中:

        H=Math.abs(end.X-current.X)+Math.abs(end.Y-current.Y),为了和下述的G值保持相同的权重,我们将H扩大了10倍,即H=H*10。

        G 是从起点A移动到指定方格的移动代价。在本例中,横向和纵向的移动代价为 10 ,对角线的移动代价为 14 。为什么是14二不是13或15呢?根据勾股定理我们可以知道:sqrt(10*10+10*10)约等于14.14,取整数就为14了。

       关于 Manhattan 方法,这很像统计从一个地点到另一个地点所穿过的街区数,而你不能斜向穿过街区。重要的是,计算 H 是,要忽略路径中的障碍物。这是对剩余距离的估算值,而不是实际值,因此才称为试探法。 把 G 和 H 相加便得到 F ,F的存在使得搜索算法有了倾向性,我们对移动代价有了一个常数的表示,这对我们算法的实现是很重要的。

        我们第一步的结果如下图所示:


第一步后的结果

每个方格都标上了 F , G , H 的值,就像起点右边的方格那样,左上角是 F ,左下角是 G ,右下角是 H 。对于这张图中的数据我们做了一些处理,图中被绿色填充的那个格子我们用correntNode表示为当前所在位置,围绕corrntNode的那八个格子,我们放入一个叫做公开列表openlist的链表中,下一次我们移动的点就是openlist中F值最小的那个节点,每次移动到下一个节点之前,我们把当前节点存放到封闭列表closelist中,同时我们在openlist中把下一节点移除,这样节点就有:openlist(min)->corrent->closelist的移动流程,需要注意的,这个过程是移动的,不能进行复制。

现在我们把所有步骤放在一起:

1. 把起点加入 open list 。

2.  重复如下a,b,c,d四个步骤:

    a. 遍历 open list ,查找 F 值最小的节点,把它作为当前要处理的节点。

    b.把这个节点移到 close list 。

    c.对当前方格的 8 个相邻方格的每一个方格?

         ◆ 如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作。

         ◆ 如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。

          ◆ 如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。

      d.  停止条件:

           ◆ 把终点加入到了 open list 中,此时路径已经找到了;

   或者◆     查找终点失败,并且 open list 是空的,此时没有路径。

  3. 保存路径。从终点开始,每个方格沿着父节点移动直至起点,这就是你的路径。  

下面是本人的对A*搜索树的java实现代码:

数据结构类Node.java

package thirty_two.A_Search;

/**

* 节点链表类

*

*/

public class Node {

public int X;

public int Y;

public int F;

public int H;

public int G;

public Node parent;

public boolean canSearch=false;  //用于判断F H G值是否给定

public void updateFG(int g) {

G=g;

F=G+H;

}

public Node(int x, int y, int f, int h, int g, Node parent) {

super();

X = x;

Y = y;

F = f;

H = h;

G = g;

this.parent = parent;

canSearch=true;

}

public Node() {

super();

}

public Node(int x, int y, int h, int g, Node parent) {

super();

X = x;

Y = y;

H = h;

G = g;

F=g+h;

this.parent = parent;

canSearch=true;

}

@Override

public String toString() {

return "Node [X=" + X + ", Y=" + Y + ", F=" + F + ", H=" + H + ", G=" + G + ", parent=" + parent

+ ", canSearch=" + canSearch + "]";

}

}

算法计算类ASearch.java:

package thirty_two.A_Search;

/**

* A*搜索算法

* 规定数字大于5的表示为障碍物

* 数字为1表示起点 数字2表示终点

* 小于0的数用作标记处理

*/

import java.util.ArrayList;

import java.util.LinkedList;

public class ASearch {

public LinkedList<Node> closelist=new LinkedList<Node>();

public LinkedList<Node> openlist=new LinkedList<Node>();

private Node start;

private Node end;

// public ASearch() {  //不允许无参构造

// super();

// }

public ASearch(Node start, Node end) {

super();

this.start = start;

this.end = end;

}

public Node getStart() {

return start;

}

public void setStart(Node start) {

this.start = start;

}

public Node getEnd() {

return end;

}

public void setEnd(Node end) {

this.end = end;

}

//检查start end 若G H F值不存在则初始化

public void checkGHF() {

if(!start.canSearch||!end.canSearch) {

start.G=0;

start.H=Math.abs(end.X-start.X)+Math.abs(end.Y-start.Y);

start.F=start.G+start.H;

end.G=0;

end.H=0;

end.F=0;

}

}

public Node findPath(int[][] map){  //搜索区域划分为二维数组

checkGHF();

Node correntNode=null;  //当前节点

//1.将start加入openlist

openlist.add(start);

map[start.X][start.Y]=-1; //标记为已加入openlist

//退出条件 openlist为空 或者 openlist将终点包含进来

while (!openlist.isEmpty()) {

//获得openlist中F最小的节点 并设为当前节点

correntNode=openlist.getFirst();

System.out.println(correntNode.toString());

//将当前节点加入closelist

closelist.add(correntNode);

openlist.remove(correntNode);

map[correntNode.X][correntNode.Y]=-2;  //标记为已加入closelist

//更新openlist

if(updateOpenlist(correntNode,map)) {

break;

};

System.out.println(openlist.size());

}

return correntNode;

}

//根据F值插入链表  维护链表

public void insert(Node node) {

if(openlist.size()==0) {

openlist.addFirst(node);

return;

}

int i=0;

while (node.F>openlist.get(i).F) {

i++;

if(i>=openlist.size()) {

openlist.addLast(node);

return ;

}

}

openlist.add(i, node);

}

//更新openlist

public boolean updateOpenlist(Node node,int[][] map) {

// openlist.clear();

//更新当前节点的openlist与原来openlist交集中数据的FG值

ArrayList<Integer> indexList=new ArrayList<Integer>();

for(int i=0;i<openlist.size();i++) {

if(Math.abs(openlist.get(i).X-node.X)==1&&Math.abs(openlist.get(i).Y-node.Y)==1) {

openlist.get(i).updateFG(14);

indexList.add(i);

}

if((Math.abs(openlist.get(i).X-node.X)==0&&Math.abs(openlist.get(i).Y-node.Y)==1)||Math.abs(openlist.get(i).X-node.X)==1&&Math.abs(openlist.get(i).Y-node.Y)==0) {

openlist.get(i).updateFG(10);

indexList.add(i);

}

}

//重排openlist 维持有序

sortOpenlist(indexList);

if(node.X>=1&&node.X<map.length-1&&node.Y>=1&&node.Y<map[0].length-1) {//a.正常情况下

int x=node.X;

int y=node.Y;

for(int i=x-1;i<=x+1;i++)

{

for(int j=y-1;j<=y+1;j++) {

if(map[i][j]==2) {

System.out.println(2);

return true;

}

if((i==x&&j==y)||map[i][j]==-2||map[i][j]>4) { 

continue;

}

//对点不可达情况

if(i!=x&&j!=y&&(map[i][y]>4||map[x][j]>4)) {

continue;

}

//当前点不存在openlist中

if(map[i][j]!=-1) {

int g=0;

if(i!=x&&j!=y) {

g=14;

}else {

g=10;

}

Node newnode=new Node(i, j, getH(i,j), g, node);

insert(newnode);

}

}

}

}

/**

* 接下来我们就要考虑如果当前点处于边界的情况了

* 在处于边界的时候,我们就检查的零接点个数就变得不确定了

*/

return false;

}

/*

* 重排openlist维持有序  基于此特殊情况的排序方式

* 排序原则:先将变动的node复制一份,在openlist去除变动过得node

* 在根据大小重新插入

*/

public void sortOpenlist(ArrayList<Integer> list) {

LinkedList<Node> temp=new LinkedList<Node>();

for (int i=list.size()-1;i>=0;i--) {

temp.add(openlist.get(list.get(i)));

openlist.remove(list.get(i));

}

openlist.clear();  //将不包含在新节点的零接节点范围的点清除

for(int i=0;i<temp.size();i++) {

insert(temp.get(i));

}

}

//计算H值

public int getH(int x,int y) {

int h=Math.abs(end.X-x)+Math.abs(end.Y-y);

return h*10;

}

public static void main(String[] args) {

Node start=new Node(2,1,40,0,null);

Node end=new Node(2,5,0,0,null);

int[][] map=new int[9][9];

for(int i=0;i<map.length;i++) {

for(int j=0;j<map[0].length;j++) {

if(i==2&&j==1) {

map[i][j]=1;

continue;

}

if(i==2&&j==5) {

map[i][j]=2;

continue;

}

map[i][j]=0;

}

}

map[1][3]=6;

map[2][3]=6;

map[3][3]=6;

for(int i=0;i<map.length;i++) {

for(int j=0;j<map[0].length;j++) {

System.out.print(map[i][j]+" ");

}

System.out.println();

}

ASearch aSearch=new ASearch(start,end);

Node node = aSearch.findPath(map);

while (node.parent!=null) {

System.out.println(node);

node=node.parent;

}

}

}

文章借鉴于:https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容