A*寻路算法,启发式搜索(超详细实现)

A*寻路算法

       关于A*算法网上优秀的文章有很多,本篇只是参考了那些文章,我自己实现了A*算法后,对A*的个人理解,在此记录下A*的实现过程,同时防止时间久了自己忘了,随时可以回来看。如有不对的地方,欢迎评论指正。

       A*启发式搜索,什么是启发式?就是给搜索的时候有一个参考,大致的方向,让搜索的时候有一定的方向性的去寻找,这样相比广度遍历要少一些搜索范围。那么A*是如何做到启发式搜索,在我看来就是那个公式F = G + H。 G:当前点到起始点路径上的消耗,H:当前点到终点的距离。通过公式可以看出F是受到G和H的影响的,可以说F越小所得到的路径就越小,所以按照F的大小顺序来进行遍历,即可获得达到终点的最短路径。下面先上代码,具体详细实现步骤和思路在代码后面。

 MapItem.java

public class MapItem{
    //父亲坐标
    private int Pre_x;
    private int Pre_y;
    //与起始位置的移动开销
    private int G;
    //预计与结束位置的移动开销
    private int H;
    //总开销
    private int F;

    public MapItem() {
        Pre_x = 0;
        Pre_y = 0;
        G = 0;
        H = 0;
        F = 0;
    }

    public int getPre_x() {
        return Pre_x;
    }

    public void setPre_x(int pre_x) {
        Pre_x = pre_x;
    }

    public int getPre_y() {
        return Pre_y;
    }

    public void setPre_y(int pre_y) {
        Pre_y = pre_y;
    }

    public int getG() {
        return G;
    }

    public void setG(int g) {
        G = g;
    }

    public int getH() {
        return H;
    }

    public void setH(int h) {
        H = h;
    }

    public int getF() {
        return F;
    }

    public void setF(int f) {
        F = f;
    }
}

这个只是记录每个格子的F值,G值,H值,前一个格子坐标用的,至于为什么记录前一个格子坐标,原因很简单,就是为了找到路径后,按照坐标回溯从而将路线展现出来。

Main.java

import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Main {
    private static final int STARTPOINT= 1;
    private static final int ENDPOINT = 2;
    private static final int WALLPOINT = 3;
    private static final int WAY_POINT = 4;
    private static final int WIDTH = 10;
    private static final int HEIGHT = 10;
    private static List<Point> openlist = new ArrayList<>();
    private static List<Point> closelist = new ArrayList<>();
    private static int map[][] = new int[HEIGHT][WIDTH];
    private static MapItem[][] items = new MapItem[HEIGHT][WIDTH];
    private static int tempx[] = {0,1,1,1,0,-1,-1,-1};//x偏移量 下,右下,右,右上,上,左上,左,左下
    private static int tempy[] = {1,1,0,-1,-1,-1,0,1};//y偏移量 下,右下,右,右上,上,左上,左,左下
    private static Point startPoint = new Point(2,3);
    private static Point endPoint = new Point(8,3);
    public static void main(String[] args){
        init();
        Astar();
        printmap(map);
    }
    //打印方法
    private static void printmap(int map[][]){
        for(int i = 0;i < HEIGHT;i++){
            for (int j = 0;j < WIDTH;j++){
                switch (map[i][j]){
                    case STARTPOINT:
                        System.out.print(" S ");
                        break;
                    case ENDPOINT:
                        System.out.print(" E ");
                        break;
                    case WALLPOINT:
                        System.out.print(" # ");
                        break;
                    case WAY_POINT:
                        System.out.print(" * ");
                        break;
                    default:
                        System.out.print("   ");
                        break;
                }
            }
            System.out.println();
        }
    }


    //初始化数据
    private static void init(){
        //G 当前点到起始点的权重
        //H 当前点到结束点的权重
        //F = G + H
        for(int i = 0;i < HEIGHT;i++){
            for(int j = 0;j < WIDTH;j++){
                items[i][j] = new MapItem();
            }
        }

        //起始point
        map[startPoint.y][startPoint.x] = STARTPOINT;
        items[startPoint.y][startPoint.x].setH(0);
        items[startPoint.y][startPoint.x].setF(0);
        items[startPoint.y][startPoint.x].setG(0);
        openlist.add(startPoint);
        //目标point
        map[endPoint.y][endPoint.x] = ENDPOINT;
        items[endPoint.y][endPoint.x].setH(0);
        items[endPoint.y][endPoint.x].setF(0);
        items[endPoint.y][endPoint.x].setG(0);

        //墙壁point
        map[1][6] = WALLPOINT;
        map[2][6] = WALLPOINT;
        map[3][6] = WALLPOINT;
        map[4][6] = WALLPOINT;
        map[5][6] = WALLPOINT;
        map[1][5] = WALLPOINT;
        map[1][4] = WALLPOINT;
        map[5][5] = WALLPOINT;
        map[5][4] = WALLPOINT;
        map[5][3] = WALLPOINT;


    }

    //A*算法
    private static void Astar(){
        //标记是否找到路径
        boolean find = false;
        //跳出条件一:open为空。说明没有路径
        while(openlist.size() != 0){
            //跳出条件二:判断Openlist里面是否有结束点,如果有说明找到路径
            if(havePointInOpenList(endPoint.x,endPoint.y)){
                find = true;
                break;
            }
            //tempPoint 当前被选中的节点,以它为中心(父节点),遍历它周围的节点。
            Point tempPoint = openlist.get(0);
            List<Point> minList = new ArrayList<>();
            //找到开启列表中F最小的。
            for(int i = 0;i < openlist.size();i++){
                Point p = openlist.get(i);
                if(items[p.y][p.x].getF() <= items[tempPoint.y][tempPoint.x].getF())
                    tempPoint = p;
            }
            //以上操作保证tempPoint 当前遍历的节点为openlist中F最小的,不用担心多个F相同节点。

            //将父节点加入close列表
            closelist.add(tempPoint);
            //将节点从openlist 中去除
            openlist.remove(tempPoint);

            //遍历八个方向
            for(int i = 0;i < 8;i++){
                int x = tempPoint.x + tempx[i];
                int y = tempPoint.y + tempy[i];
                //判断墙壁,障碍物,在关闭列表的。放弃遍历
                if(x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT || map[y][x] == WALLPOINT || havePointInCloseList(x,y))
                    continue;

                //计算G
                int gLeng = items[tempPoint.y][tempPoint.x].getG();
                if(i % 2 == 0){
                    gLeng = gLeng + 10;
                }else{
                    gLeng = gLeng + 14;
                }

                //在开放列表当中,说明该节点已经被遍历过了
                if(havePointInOpenList(x,y)){
                    //判断当前被遍历的节点是否有最优路线
                    //判定方法:当前节点到父节点(tempPoint)的G值   与   当前节点本身已拥有的G值   相比较。
                    //若到父节点(tempPoint)G值   小于   当前节点以拥有的G值
                    //说明当前节点到新的父节点路线最优,应当更改父节点的记录为新的父节点坐标。
                    //由于此时H值是不会变化的,所以不用考虑H值,此时影响F的大小是G。
                    if(gLeng < items[y][x].getG()){
                        items[y][x].setPre_x(tempPoint.x);
                        items[y][x].setPre_y(tempPoint.y);
                        items[y][x].setG(gLeng);
                        items[y][x].setF(items[y][x].getH() + items[y][x].getG());
                    }
                }else{
                    //不在开放列表当中
                    //将F,H,G进行计算并赋值,以tempPoint为父节点记录父节点坐标,加入Openlist当中。
                    int hLeng = Math.abs(endPoint.x - x) + Math.abs(endPoint.y - y);
                    hLeng = hLeng * 10;
                    items[y][x].setPre_x(tempPoint.x);
                    items[y][x].setPre_y(tempPoint.y);
                    items[y][x].setG(gLeng);
                    items[y][x].setH(hLeng);
                    items[y][x].setF(items[y][x].getH() + items[y][x].getG());
                    Point p = new Point(x,y);
                    //加入open列表
                    openlist.add(p);
                }

            }

        }

        //将找到的路线填充到Map中去,方便显示遍历,这块并不是那么重要,就只是回溯,一直到起点。
        //将结果写入Map中去。
        int currentx = items[endPoint.y][endPoint.x].getPre_x();
        int currenty = items[endPoint.y][endPoint.x].getPre_y();
        if(find){
            while(currentx != startPoint.x || currenty != startPoint.y){
                map[currenty][currentx] = WAY_POINT;
                MapItem item = items[currenty][currentx];
                currentx = item.getPre_x();
                currenty = item.getPre_y();
            }
        }else{
            System.out.println("抱歉没有找到路径");
        }


    }

    private static boolean havePointInCloseList(int x,int y){
        for(Point p : closelist){
            if(p.x == x && p.y == y)
                return true;
        }
        return false;
    }

    private static boolean havePointInOpenList(int x,int y){
        for(Point p : openlist){
            if(p.x == x && p.y == y)
                return true;
        }
        return false;
    }
}

Ok,全部代码就在这里了。

       Openlist :开放列表,存放预选节点,也就是预选列表,选择下一个被遍历节点就是从这里面选的。

       Closelist:关闭列表,存放已经遍历过的节点,也就是说已经确定了,被检查过周围节点的节点,确定了周围节点都是最优路线的,这个节点不能被重复遍历的。

       关于F,G,H计算:F = G + H,G的话就是从开始到当前点的消耗,上下左右四个方向走一步距离为1,斜对角那么就是约为1.4,为了方便计算就都扩大10倍,10和14。其实不用那么严格,只要保证两边之和大于第三边即可,朝正方向的步长小于斜对角长度。  H的距离的话,愿意用勾股定理计算也可以,不过为了方便我还是用的x向的距离加上y向的距离。也可表示距离长度。没什么影响的。

大体思路:

       将起始节点加入openlist当中,然后就进入循环查找路线。一个循环过程:

       1.从openlist当中找F值为最小的节点作为父节点然后遍历周围的节点,若有F相同选择G最小的

       2.将父节点从openlist中移除,加入到closelist当中

       3.遍历父节点周围的节点,如果是墙壁或边界,跳过该节点。如果在closelist当中,跳过该节点。

       4.如果该节点(父节点周围的节点)不在openlist当中,计算F,G,H,并记录父节点坐标,G值为父节点的G值加上到达当前点的步长消耗。(因为这种节点,第一次遍历到,那么到达它目前的最优路径就是通过父亲节点到达)。

       5.如果该节点在openlist当中,说明他也是待选节点,他已经被遍历过了,那么他就有了选择,选择原先的父节点,还是当前的父节点,这取决于G值,因为此时H值是不会变的,G值越小消耗越小,F就会越小,路径会越短。那么F的大小决定了优先级,决定了该节点被当做父节点的优先级。F越小优先级越高,优先级越高被先遍历到的可能性越大。(这块就是启发式搜索,我个人感觉这个F起到优先级作用,改变了搜索的优先顺序,从而使得寻路更加的高效。)

       6.重复以上步骤,直到openlist为空(为什么会空呢,当开始节点在一个被障碍物组成的封闭空间内,边界被跳过,closelist被跳过,每次循环都有节点放入close当中,那么封闭空间内节点终将会被遍历完,全部放入closelist中,自然就为空了),或者目标节点放入了openlist(找到路径)。

我写的时候一些担心和小问题:

1.F值相同的情况

       不用担心F值相同的节点,因为都相同的话,位置不同,每次遍历都是优先选择F最小的,终会遍历完所有的相同F的坐标,那么它如果离终点较远的话,要知道H的值是不会改变的,他的G值会变得很大,导致F也会变得很大,那么它周围的节点很大可能是不会被下一次循环选中作为父节点的。

2.遍历周围节点太麻烦

        当初最早的时候还傻乎乎的用一堆if去判断方向,利用偏移量,这个思路我也忘了是从哪里学过来的。自己先定好一个遍历顺序,顺时针啊逆时针啊随你,然后按照顺序将x,y的偏移写到两个数组中去,反正就装起。遍历的时候,循环这个数组,只用写一个当前节点坐标加上偏移量即可,计算G值也很简单,由于遍历是有顺序的,那么就可以通过循环次数的奇偶性判断是对角线还是正方向,对应加上值即可。

以上就是思路,至于代码详细解释,emmm...代码里的注释应该写的比较清楚吧,havePointInCloseList(),havePointInOpenList(),就是判断节点是否在关闭列表和开启列表内用的。

       

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值