算法介绍
A*算法是指寻找当前位置到目标位子的最短路径,整体思路是,以格子来表示位置的话,通过两个列表:open和close来存储可移动的格子和已经处理过的的格子,每次从open中拿出来一个格子,判断该格子四周的格子是否是可到达的(如果超出地图边界或者遇到障碍物,则跳过),是就将其加入到open中,并将当前拿出来的格子放入close中;在close中的格子无需再次处理;
整个处理过程中用大了一个公式来处理挑选open中的最佳格子:F=G+H;
G: 表示当前格子到目标格子的实际距离(距离就是当前格子走到目标格子需要移动几步,如果只能上下左右移动,我们可以考虑每次移动的距离都是1,如果可以向左上、右上、左下、右下移动,那可以考虑上下左右移动为10,斜向移动为14)
H:表示当前格子到目标格子的估算距离,可以考虑使用曼哈顿距离:|x1-x2|+|y1-y2|;
F:表示当前格子到目标格子的最终估算距离,越小表示路径越短;
算法实现
-
辅助列表:open 和close列表
open:用于记录待访问的节点(可以使用优先队列PriorityQueue,以便对节点自动排序)
close:用于记录已访问的节点 -
计算开始节点周围可移动的节点
- 从起始节点开始,想四周发散计算可移动的相邻节点,并将相邻节点加入open列表,此过程可以排除障碍物、以及超出地图限制的节点;
- 相邻节点可以是:垂直与水平方向的节点;也可以根据要求是对角线方向的节点;
- 结束条件:当目标节点在close中或者open列表为空(已经遍历了所有可遍历的节点)
- 详细实现:
- 将起始节点start加入open
- 遍历open:当open不为空时,每次从open中取出F最小值的节点作为当前节点,然后计算当前节点相邻的可移动的邻节点,将可移动的邻节点加入open,最后将当前节点放入close中;
- 邻节点加入open需要由一定规则判断:
-
邻节点不在地图范围,不加入open;
-
邻节点是障碍物,不加入open;
-
邻节点在close中,不加入open;
-
邻节点不再open中,将邻节点加入open,并将该邻节点的父节点设置为当前节点;(父节点的作用是在结束时,根据父节点回溯出整个移动路径);
-
邻节点在open中,需要进行判断:如果邻节点(open中的节点)的G值(起点到当前节点的实际路径代价)大于当前节点的G值+移动到该邻节点的实际路径代价,那么修改该邻节点的父节点为当前节点,修改该邻节点的G值为当前节点的G值+当前节点的G值移动到该邻节点的实际路径代价;对于第5点,举个例子,如下图:
- 假如目标节点[19,18],起点[21,21],那么目标节点[19,18]既是节点[20,18]的相邻节点,又是节点[18,18]的相邻节点;
- 如果到达节点[18,18]时开始计算相邻节点,得到了[19,18],准备将[19,18]加入open中;
- 此时如果open中没有[19,18],那么直接加入;
- 如果open中已经有[19,18](比如是作为[20,18]的相邻节点加入的,当然也可以是作为其他节点的相邻节点加入的),则需要对[19,18]的G值进行判断:open中的[19,18]的G值是5(图中灰色路径,每次移动代价为1);而从[18,18]的G值是:6,[18,18]移动到[19,18]需要加1,即从[18,18]移动到[19,18]的实际路径代价G是6+1=7;7大于5,所以从[20,18]移动到[19,18]是最优的,那么需要将[19,18]的父节点设置为[20,18];后续别的节点也计算到open中有相同的邻节点,也是这个判断;
-
-
终止条件:当目标节点在close中或者open列表为空(已经遍历了所有可遍历的节点)
-
通过回溯,从close中取出目标节点,回溯其父节点路径得到最优的路径;
-
代码实现
- 声明open和close列表
public static PriorityQueue<Box> open = new PriorityQueue<>();
public static List<Box> close = new ArrayList<>();
- 声明box的辅助类
package com.vander.datastructureandalgorithm.backtrack;
import java.util.Objects;
/**
* 可移动格子类
* 本例中,障碍物的判断是用list<Box>来表示障碍物集合的,也可直接在box类中添加一个标识,是否是障碍物
* 来区分
*/
public class Box implements Comparable<Box>{
int x;
int y;
int g;
int h;
int f;
Box parent;
public Box(int x,int y){
this.x = x;
this.y = y;
}
public Box(int x,int y,int g,Box parent){
this.x = x;
this.y = y;
this.g = g;
this.parent = parent;
}
public Box(int x, int y, int g, int h, int f, Box parent) {
this.x = x;
this.y = y;
this.g = g;
this.h = h;
this.f = f;
this.parent = parent;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getG() {
return g;
}
public void setG(int g) {
this.g = g;
}
public int getH() {
return h;
}
public void setH(int h) {
this.h = h;
}
public int getF() {
return f;
}
public void setF(int f) {
this.f = f;
}
public Box getParent() {
return parent;
}
public void setParent(Box parent) {
this.parent = parent;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Box box = (Box) o;
//只需要判断格子是否具有相同坐标即可
return x == box.x && y == box.y ;
}
@Override
public int hashCode() {
return Objects.hash(x, y, g, h, f, parent);
}
@Override
public int compareTo(Box o) {
//根据f值来比较,比较小的优先
return this.getF()-o.getF();
}
public String toString2(){
return "["+this.getX()+","+this.getY()+"]";
}
public String toString(){
return "["+this.getX()+","+this.getY()+"],f="+this.getF();
}
}
- 声明一些辅助函数
/**
* 打印open列表,便于调试
* @return
*/
private static String showOpen(){
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Box box:open){
//System.out.println(open.toString());
sb.append(box.getX()+"~"+box.getY()+"f("+box.getF()+"),");
}
return sb.substring(0, sb.length()-1)+"]";
}
/**
* 把current上下左右四个box加入open列表
* 本例中移动限制只能水平或者垂直,也可以根据实际情况
* 增加对角线方向的移动,只要计算好下一个节点的坐标以及移动代价就行
* (个人觉得可以直线方向移动设置为1,对角线方向设置为2,但是别的教程都说是对角线设置为14,水平设置为10)
*
*/
public static void addToOpenPre(Box current,Box target,List<Box> obss){
int cx = current.getX();
int cy = current.getY();
int cg = current.getG();
int nextG = cg+1;
//获取当前box 上下左右四个格子
Box up = new Box(cx, cy-1, nextG,current);
//计算box的h和f
calH(up, target);
addToOpen(up,obss,target);
Box down = new Box(cx, cy+1, nextG, current);
calH(down, target);
addToOpen(down,obss,target);
//左
Box left = new Box(cx-1, cy, nextG, current);
calH(left, target);
addToOpen(left,obss,target);
//右
Box right = new Box(cx+1, cy, nextG, current);
calH(right, target);
addToOpen(right,obss,target);
}
/**
* 路径寻找完毕后,用于回溯路径,
* 注意,回溯是倒叙的即是从终点---到起点
* @param target
*/
public static void drawPaht(Box target){
while (target!=null){
System.out.print("["+ target.getX()+","+ target.getY()+"]");
target = target.getParent();
}
}
/**
* 如果box在close中,则跳过
* 如果 box 不在open中,则直接将box加入open
* 如果box在open中,则需要进行判断:
*
* @param nextBox 当前节点的邻节点
* @param obss
*/
public static void addToOpen(Box nextBox,List<Box> obss ,Box target){
//超出地图边界
if (nextBox.getX()>21||nextBox.getY()>21||nextBox.getX()<1||nextBox.getY()<1){
return;
}
if (isObs(nextBox,obss)){
return;
}
if (inClose(nextBox)){
return;
}
//看下邻节点是否已经在open中
Box child = inOpen(nextBox);
if (child==null){
//不在open
if (nextBox.equals(target)){
//如果要加入的邻节点是目标节点,则设置加入的邻节点为目标节点的父节点
child = target;
child.setParent(nextBox);
child.setG(nextBox.getG());
child.setH(nextBox.getH());
}else {
//要加入的邻节点不是目标节点,则直接加入open即可
child = nextBox;
}
System.out.println("将box["+nextBox.getX()+","+nextBox.getY()+"] 加入开放open");
open.add(child);
}else if (child.getG()>nextBox.getG()){
//加入的邻节点在open中且open中对应相同的节点的G值大于要加入的邻节点的G值,
//那么替换open中的对应的节点的G值为要加入的邻节点的G值,并设置open对应节点的父节点为
//要加入的邻节点的父节点
child.setG(nextBox.getG());
//child.parent = box;
child.parent = nextBox.getParent();
//这个add可以不用,因为此时child本来就在open中
//open.add(child);
}
}
/**
* 计算给定盒子的
* f = 总预估距离
* g = 起点到指定节点移动距离,本处可以直接使用box的g,因为从起点
* 到指定节点(即此处的box)g是一直累加上来的;
* h = 指定节点(此处的box)到目标距离
* @param box
* @param target
*/
public static void calH(Box box,Box target){
int h = Math.abs(box.getX()-target.getX())+Math.abs(box.getY()-target.getY());
box.setH(h);
int f = h+box.getG();
box.setF(f);
}
/**
* 在结束列表中
* @param current
* @return
*/
public static boolean inClose(Box current){
for (Box box:close){
if (current.equals(box)){
return true;
}
}
return false;
}
/**
* 在open列表中
* @param current
* @return
*/
public static Box inOpen(Box current ){
for (Box box:open){
if (current.equals(box)){
return box;
}
}
return null;
}
/**
* 是否是障碍物
* @param box
* @param obss 障碍物集合
* @return
*/
private static boolean isObs(Box box,List<Box> obss){
if (box.getX()==19&&box.getY()==16){
System.out.println(box);
}
for (Box obs:obss){
if (box.equals(obs)){
return true;
}
}
return false;
}
- 主方法
public static void main(String[] args) {
List<Box> obss = new ArrayList<>();
Box o1 = new Box(13, 21);
Box o2 = new Box(13,20);
Box o3 = new Box(13,19);
Box o4 = new Box(13,18);
Box o5 = new Box(13,17);
Box o6 = new Box(13,16);
Box o7 = new Box(14,16);
Box o8 = new Box(15,16);
Box o9 = new Box(16,16);
Box o10 = new Box(18,16);
Box o11= new Box(19,16);
Box o12= new Box(20,16);
Box o13= new Box(21,16);
Box o14= new Box(14,12);
Box o15= new Box(13,13);
obss.add(o1);
obss.add(o2);
obss.add(o3);
obss.add(o4);
obss.add(o5);
obss.add(o6);
obss.add(o7);
obss.add(o8);
obss.add(o9);
obss.add(o10);
obss.add(o11);
obss.add(o12);
obss.add(o13);
obss.add(o14);
obss.add(o15);
//------------------------
Box target = new Box(13, 12, 0, null);
Box current = new Box(21,21, 0, null);
calH(current, target);
//先把起点加入open
open.add(current);
//遍历open将所有符合条件的box加入open
while (!open.isEmpty()){
Box current1 = open.poll();
//将current 加入close
close.add(current1);
//将A周围的box加入open
addToOpenPre(current1,target,obss);
System.out.println("box["+current1.getX()+","+current1.getY()+"]四周格子加入完毕,此时已加入格子有:"+ showOpen());
//如果该节点时终点,则终止,并将终点加入close
if (current1.equals(target)){
//此函数打印的就是从起点到目标节点的路径,可以根据实际情况,在此处返回路径信息
drawPaht(current1);
break;
}
}
}
- 效果(当前位置[21,21],目标[13,12]):
//注意输出的结果顺序是从目标到起点的
[13,12][13,12][12,12][12,13][12,14][13,14][14,14][15,14][16,14][17,14][17,15][17,16][17,17][17,18][18,18][19,18][19,19][19,20][20,20][21,20][21,21]
参考链接:java实现路径规划(A*算法)