A*算法实践

算法介绍

A*算法是指寻找当前位置到目标位子的最短路径,整体思路是,以格子来表示位置的话,通过两个列表:open和close来存储可移动的格子和已经处理过的的格子,每次从open中拿出来一个格子,判断该格子四周的格子是否是可到达的(如果超出地图边界或者遇到障碍物,则跳过),是就将其加入到open中,并将当前拿出来的格子放入close中;在close中的格子无需再次处理;
整个处理过程中用大了一个公式来处理挑选open中的最佳格子:F=G+H;
G: 表示当前格子到目标格子的实际距离(距离就是当前格子走到目标格子需要移动几步,如果只能上下左右移动,我们可以考虑每次移动的距离都是1,如果可以向左上、右上、左下、右下移动,那可以考虑上下左右移动为10,斜向移动为14)
H:表示当前格子到目标格子的估算距离,可以考虑使用曼哈顿距离:|x1-x2|+|y1-y2|;
F:表示当前格子到目标格子的最终估算距离,越小表示路径越短;

算法实现

  1. 辅助列表:open 和close列表
    open:用于记录待访问的节点(可以使用优先队列PriorityQueue,以便对节点自动排序)
    close:用于记录已访问的节点

  2. 计算开始节点周围可移动的节点

    • 从起始节点开始,想四周发散计算可移动的相邻节点,并将相邻节点加入open列表,此过程可以排除障碍物、以及超出地图限制的节点;
    • 相邻节点可以是:垂直与水平方向的节点;也可以根据要求是对角线方向的节点;
    • 结束条件:当目标节点在close中或者open列表为空(已经遍历了所有可遍历的节点)
    • 详细实现
      1. 将起始节点start加入open
      2. 遍历open:当open不为空时,每次从open中取出F最小值的节点作为当前节点,然后计算当前节点相邻的可移动的邻节点,将可移动的邻节点加入open,最后将当前节点放入close中;
      3. 邻节点加入open需要由一定规则判断:
        1. 邻节点不在地图范围,不加入open;

        2. 邻节点是障碍物,不加入open;

        3. 邻节点在close中,不加入open;

        4. 邻节点不再open中,将邻节点加入open,并将该邻节点的父节点设置为当前节点;(父节点的作用是在结束时,根据父节点回溯出整个移动路径);

        5. 邻节点在open中,需要进行判断:如果邻节点(open中的节点)的G值(起点到当前节点的实际路径代价)大于当前节点的G值+移动到该邻节点的实际路径代价,那么修改该邻节点的父节点为当前节点,修改该邻节点的G值为当前节点的G值+当前节点的G值移动到该邻节点的实际路径代价;对于第5点,举个例子,如下图:
          起点:21,21;目标:18,18

          1. 假如目标节点[19,18],起点[21,21],那么目标节点[19,18]既是节点[20,18]的相邻节点,又是节点[18,18]的相邻节点;
          2. 如果到达节点[18,18]时开始计算相邻节点,得到了[19,18],准备将[19,18]加入open中;
          3. 此时如果open中没有[19,18],那么直接加入;
          4. 如果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中有相同的邻节点,也是这个判断;
  3. 终止条件:当目标节点在close中或者open列表为空(已经遍历了所有可遍历的节点)

  4. 通过回溯,从close中取出目标节点,回溯其父节点路径得到最优的路径;

  5. 代码实现

    1. 声明open和close列表
public static PriorityQueue<Box> open = new PriorityQueue<>();

public static List<Box> close = new ArrayList<>();
  1. 声明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();
    }
}

  1. 声明一些辅助函数
/**
 * 打印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;
}
  1. 主方法
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;
        }
    }
}
  1. 效果(当前位置[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*算法)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值