基本原理

  • 简单的说,A*算法就是不停的从起点开始遍历周围的点,找出目前来说消耗最小的点作为新的起点,直到找到终点。
  • 如何找到消耗最小的点
    • f(消耗)=g(离起点距离)+h(离终点距离)
    • g:从起点到达当前点要走的距离,上下左右都是1,斜边用勾股定理算出约为1.4
    • h:按照曼哈顿距离计算(d(i,j)=|X1-X2|+|Y1-Y2| )
  • 开启列表:用来存储可以考虑行进的点,同时存放f、g、h、父对象(从哪个点来)的信息。
  • 关闭列表:用来存储不再考虑的点,存放在关闭列表中的点需要从开启列表移除,同时存放f、g、h、父对象(从哪个点来)的信息。
  • 如何确定路径:
    • 虽然最优点都在关闭列表中,但是并不能直接使用关闭列表中的点作为路径。
    • 而是在关闭列表中从终点开始查找父对象,再查找父对象的父对象
  • 死路:当开启列表为空的时候,说明已经找遍了所有可能的点,寻路失败。
  • 详细流程:
    • 1、将起点记录为当前点a
    • 2、将当前点a放入关闭列表,并设置父对象为空
    • 3、将当前点a周围所有能行进的格子放入开启列表,如果周围的点已经在开启列表或者关闭列表中,就不用管它了
    • 4、记录当前点a周围所有能行进的格子的f值和父对象(父对象为当前点)
    • 5、寻找最优点将其放入关闭列表,并删除开启列表中的最优点,每次往关闭列表放点时,要判断该点是否为终点,如果是证明路径已经找完了,跳到步骤8,如果不是则继续步骤6
    • 6、将最优点作为起点b
    • 7、跳回步骤3
    • 8、找到终点,寻路结束
  • 拓展阅读:

题目要求

  • 使用A*算法实现自动寻路
  • 实现地图数据存取

代码实现

Maze类

package Maze;
class Maze
{
    static final int ROWS = 20;
    static final int COLS = 20;
    Node map[][] = new Node[ROWS][COLS];
    Node start = null;
    Node end = null;
    //储存当前所有的测试点信息的集合
    PathList test = new PathList();
    //储存最短路径信息的集合
    Path select = new Path();
    //初始化地图
    void init()
    {
        for(int i=0;i<ROWS;i++)
        {
            for(int j=0;j<COLS;j++)
            {
                map[i][j] = new Node(i,j,1);
            }
        }
    }
    void gameStart(MazePanel panel)
    {
        //node为当前点
        Node node = start;
        //将当前节点储存在列表中
        select.addNode(start,0);
        //死循环,直到到达end点
        while(true)
        {
            System.out.println("当前坐标:(" + node.row + "," + node.col + ")" +
                    "
测试点的数量:"+test.list.size() +
                    "
已经走了几步:"+(select.list.size() -1) +
                    "
当前点是否被访问:" + node.visit);
            //测试点测试:将当前点标记为被访问过,将未被访问的相邻点加入到表中并录入路径信息
            test(node);

            panel.repaint(0,0,700,700);
            panel.update(panel.getGraphics());
            //80ms刷新一次
            try
            {
                Thread.sleep(80);
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
            //获取所有当前测试点中cost+forecast最小的点并设置为当前点
            select = test.selectPath();
            node = (Node)select.list.get(select.list.size()-1);
            //如果找到已经到了目标点则将当前点设置为已访问并跳出死循环
            if((Math.abs(node.row-end.row)<=1) && (Math.abs(node.col-end.col)<=1))
            {
                node.visit = 1;
                break;
            }
        }
        System.out.println("找到最短路径,下面为最短路径坐标");
        select.addNode(end,0);
        //遍历最短路径
        for(int i=0;i<select.list.size();i++)
        {
            //输出最短路径的过程坐标
            node = (Node)select.list.get(i);
            System.out.println("(" + node.row + "," + node.col + ")");
        }
    }
    //测试点
    void test(Node node)
    {
        int row = node.row;
        int col = node.col;
        //标记当前位置已经到达
        //node.visit = 1;
        map[node.row][node.col].visit = 1;
        for(int i=-1;i<=1;i++)
        {
            //行数-1小于0或者行+i大于20(超出地图)就跳出当前循环
            if((row+i<0) || (row+i>=ROWS))
                continue;
            for(int j=-1;j<=1;j++)
            {
                if((col+j<0) || (col+j>=COLS))
                    continue;
                //将当前点的周围标记为测试点
                map[row+i][col+j].test = 1;
                //若测试点没有被访问过(有的点可能在之前的遍历中被访问了)
                if(map[row+i][col+j].visit == 0)
                {
                    //将当前测试点坐标存入temp中
                    Node temp = map[row+i][col+j];
                    //将select中的cost和forecast、list存入新的路径path
                    Path path = new Path(select);
                    //算出预测点与终点的距离(曼哈顿距离)
                    int forecast = Math.abs(end.row - temp.row) + Math.abs(end.col - temp.col);
                    path.addNode(temp,forecast);
                    test.addPath(path);
                }
            }
        }
    }
}

MazeFrame类

package Maze;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//gui界面初始化、监听
class MazeFrame extends JFrame
{
    //墙的数量
    int count[] = new int[]{0,0};
    String[] wallStr = new String[400];

    MazePanel mazePanel = null;
    Maze maze = null;
    MazeFrame(Maze maze)
    {
        this.maze = maze;
        mazePanel = new MazePanel(maze);

        setTitle("Maze");
        setSize(620,670);
        //初始化GUI画面
        initView();
        setVisible(true);
    }

    void initView()
    {
        getContentPane().setLayout(new BorderLayout());
        //鼠标点击监听、画图
        mazePanel.addMouseListener(new MouseAdapter()
        {
            public void mouseClicked(MouseEvent e)
            {
                int x = e.getX();
                int y = e.getY();
                //鼠标点击某行列
                int row = y / MazePanel.WIDTH;
                int col = x / MazePanel.WIDTH;
                //鼠标按第一次是开始点
                if(mazePanel.clickCount == 0)
                {
                    mazePanel.maze.start = mazePanel.maze.map[row][col];
                }
                else if(mazePanel.clickCount == 1)
                {
                    mazePanel.maze.end = mazePanel.maze.map[row][col];
                }
                else
                {
                    mazePanel.maze.map[row][col].cost = 100;
                    wallStr [count[0]] =  "(" + String.valueOf(row) +"," + String.valueOf(col) + ")";
                    count[0] ++;
                }

                mazePanel.repaint(0,0,700,700);
                mazePanel.clickCount++;
            }
        });
        //在GUI窗口中增加迷宫面板和ok按钮
        getContentPane().add(mazePanel,BorderLayout.CENTER);
        JPanel bottomPanel = new JPanel(new FlowLayout());
        JButton startButton = new JButton("Start");
        JButton saveButton = new JButton("SaveMap");
        JButton loadButton = new JButton("LoadMap");
        TextField pathText = new TextField(30);
        JButton restartButton = new JButton("ReStart");
        add(bottomPanel,BorderLayout.SOUTH);

        startButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                //开始
                mazePanel.repaint(0,0,700,700);
                maze.gameStart(mazePanel);
            }
        });
        bottomPanel.add(startButton);

        bottomPanel.add(pathText);
        //储存数据
        saveButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String path = pathText.getText();
                try {
                    FileOutputStream fos = new FileOutputStream(path,false);
                    BufferedOutputStream bos = new BufferedOutputStream(fos);
                    String startStr = "("
                             + String.valueOf(mazePanel.maze.start.row)
                             +","
                             + String.valueOf(mazePanel.maze.start.col)
                             + ")";
                    String endStr = "("
                            + String.valueOf(mazePanel.maze.end.row)
                            +","
                            + String.valueOf(mazePanel.maze.end.col)
                            + ")";
                    byte[] startByte = new byte[16];
                    byte[] endByte = new byte[16];
                    byte[] wallByte = new byte[1024];
                    startByte = startStr.getBytes(StandardCharsets.UTF_8);
                    endByte = endStr.getBytes(StandardCharsets.UTF_8);
                    bos.write(startByte);
                    bos.write(endByte);
                    for (int i = 0;i < count[0];i++)
                    {
                        wallByte = wallStr[i].getBytes(StandardCharsets.UTF_8);
                        bos.write(wallByte);
                    }
                    bos.close();
                }catch (Exception exception)
                {
                    exception.printStackTrace();
                }
            }
        });
        bottomPanel.add(saveButton);
        //加载数据
        loadButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String path = pathText.getText();
                try {
                    FileInputStream fis = new FileInputStream(path);
                    byte[] bytes = new byte[10240];
                    int flag = 0;
                    fis.read(bytes);
                    String str = new String(bytes);
                    String regex="[0-9]+(\\.[0-9]+)?";
                    Pattern p = Pattern.compile(regex);
                    Matcher m = p.matcher(str);
                    int temp = 0;
                    while (m.find())
                    {
                        switch (flag)
                        {
                            case 0:
                            {
                                temp = Integer.parseInt(m.group());
                                flag = 1;
                                break;
                            }
                            case 1:
                            {
                                mazePanel.maze.start = mazePanel.maze.map[temp][Integer.parseInt(m.group())];
                                flag = 2;
                                break;
                            }
                            case 2:
                            {
                                temp = Integer.parseInt(m.group());
                                flag = 3;
                                break;
                            }
                            case 3:
                            {
                                mazePanel.maze.end = mazePanel.maze.map[temp][Integer.parseInt(m.group())];
                                flag = 4;
                                break;
                            }
                            case 4:
                            {
                                temp = Integer.parseInt(m.group());
                                flag = 5;
                                break;
                            }
                            case 5:
                            {
                                mazePanel.maze.map[temp][Integer.parseInt(m.group())].cost = 100;
                                count[1]++;
                                if (count[1] == count[0])
                                {
                                    System.out.println(count[1]);
                                    flag = 6;
                                    break;
                                }else
                                {
                                    flag = 4;
                                    break;
                                }
                            }
                        }
                        if (flag == 6)
                        {
                            break;
                        }
                    }
                    mazePanel.repaint(0,0,700,700);
                }catch (Exception exception)
                {
                    exception.printStackTrace();
                }
            }
        });
        bottomPanel.add(loadButton);
        //重启按钮
        restartButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setVisible(false);
                Maze maze = new Maze();
                maze.init();
                MazeFrame frame = new MazeFrame(maze);
            }
        });
        bottomPanel.add(restartButton);
    }
}

MazePanel类

package Maze;
import javax.swing.*;
import java.awt.*;
//迷宫的GUI实现
class MazePanel extends JPanel
{
    Maze maze = null;
    int clickCount = 0;
    //定义一系列常量
    static final int WIDTH = 30;

    static final Color START_COLOR = Color.green;
    static final Color END_COLOR = Color.red;
    static final Color DEFAULT_COLOR = Color.white;
    static final Color WALL_COLOR = Color.gray;
    static final Color TEST_COLOR = Color.yellow;
    static final Color VISIT_COLOR = Color.pink;
    static final Color PATH_COLOR = Color.blue;
    //构造函数
    MazePanel(Maze maze)
    {
        this.maze = maze;
    }

    public void paintComponent(Graphics g)
    {
        //默认边框
        g.clearRect(0,0,700,700);
        g.setColor(Color.black);
        for(int i=0;i<=Maze.ROWS;i++)
        {
            g.drawLine(0,WIDTH*i,WIDTH* Maze.COLS,WIDTH*i);

        }
        for(int j = 0; j<= Maze.COLS; j++)
        {
            g.drawLine(WIDTH*j,0,WIDTH*j,WIDTH* Maze.ROWS);
        }
        //测试(当前点的四周)
        g.setColor(TEST_COLOR);
        for(int i = 0; i< Maze.ROWS; i++)
        {
            for(int j = 0; j< Maze.COLS; j++)
            {
                if(maze.map[i][j].test == 1)
                {
                    //覆盖四周八个点
                    g.fillRect(maze.map[i][j].col*WIDTH+1,maze.map[i][j].row*WIDTH+1,WIDTH-1,WIDTH-1);

                }
            }
        }
        //已经到达的点
        g.setColor(VISIT_COLOR);
        for(int i = 0; i< Maze.ROWS; i++)
        {
            for(int j = 0; j< Maze.COLS; j++)
            {
                if(maze.map[i][j].visit == 1)
                {
                    g.fillRect(maze.map[i][j].col*WIDTH+1,maze.map[i][j].row*WIDTH+1,WIDTH-1,WIDTH-1);
                }
            }
        }
        //墙壁的cost为一个巨大的值
        g.setColor(WALL_COLOR);
        for(int i = 0; i< Maze.ROWS; i++)
        {
            for(int j = 0; j< Maze.COLS; j++)
            {
                if(maze.map[i][j].cost == 100)
                {
                    g.fillRect(maze.map[i][j].col*WIDTH+1,maze.map[i][j].row*WIDTH+1,WIDTH-1,WIDTH-1);
                }
            }
        }

        if(maze.start != null)
        {
            g.setColor(START_COLOR);
            g.fillRect(maze.start.col*WIDTH+1,maze.start.row*WIDTH+1,WIDTH-1,WIDTH-1);
        }

        if(maze.end != null)
        {
            g.setColor(END_COLOR);
            g.fillRect(maze.end.col*WIDTH+1,maze.end.row*WIDTH+1,WIDTH-1,WIDTH-1);
        }
        //画最短路径线
        int length = maze.select.list.size();
        if(length > 0)
        {
            //遍历中的当前点
            Node node = (Node)maze.select.list.get(length - 1);
            //只有找到最短路径才开始画线
            if(node.equals(maze.end) == true)
            {
                Graphics2D g2d = (Graphics2D)g;
                g2d.setColor(PATH_COLOR);
                g2d.setStroke(new BasicStroke(2.0f));

                int startX = maze.start.col * WIDTH + WIDTH/2;
                int startY = maze.start.row * WIDTH + WIDTH/2;

                for(int i=1;i<length;i++)
                {
                    node = (Node)maze.select.list.get(i);

                    int endX = node.col * WIDTH + WIDTH/2;
                    int endY = node.row * WIDTH + WIDTH/2;

                    g2d.drawLine(startX,startY,endX,endY);

                    startX = endX;
                    startY = endY;
                }
            }
        }
    }
}

Node类

package Maze;
class Node
{
    int row = 0;
    int col = 0;
    int cost = 0;
    int test = 0;
    int visit = 0;
    //节点的构造函数,储存当前行列信息,cost为走一步的消耗
    Node()
    {
        //
    }
    Node(int row,int col,int cost)
    {
        this.row = row;
        this.col = col;
        this.cost = cost;
    }
    //当前点与终点是否为一个点
    public boolean equals(Object object)
    {
        if(!(object instanceof Node))
            return false;

        Node node = (Node)object;
        if((node.row == row) && (node.col == col))
        {
            return true;
        }

        else
            return false;
    }
}

Path类

package Maze;
import java.util.Vector;
//路径信息类,forecast为测试点曼哈顿距离的值,cost为当前点已经经走过的路程+1
class Path
{
    int cost = 0;
    int forecast = 0;
    Vector list = new Vector();
    Path()
    {
    }
    //将(select)节点的路径信息依次存进表中
    Path(Path path)
    {
        for(int i=0;i<path.list.size();i++)
        {
            list.add(path.list.get(i));
        }

        this.cost = path.cost;
        this.forecast = path.forecast;
    }
    void addNode(Node node,int forecast)
    {
        list.add(node);
        //若经过墙壁cost+=100否则+1
        this.cost += node.cost;
        this.forecast = forecast;
    }
}

PathList类

package Maze;
import java.util.Vector;
class PathList
{
    Vector list = new Vector();
    void addPath(Path path)//增加path并按照cost+forecast排序
    {
        int i = 0;
        //将当前表中cost+forecast与传入的值做对比,若传入值小于原本位置的值就插入到表的当前位置
        for(i=0;i<list.size();i++)
        {
            int cost = ((Path)list.get(i)).cost;
            int forecast = ((Path)list.get(i)).forecast;
            if((cost+forecast) > (path.cost+path.forecast))
            {
                break;
            }
        }
        //插入
        list.insertElementAt(path,i);
    }
    Path selectPath()// 找到首个在表中未被访问的点并返回该点在表中的位置(该点一定是cost+forecast最小的点)
    {
        int result = 0;
        //遍历当前表
        for(int i=0;i<list.size();i++)
        {
            //将各个路径信息存到新建的路径中
            Path path = (Path)list.get(i);
            //将各点的信息存到新建的节点中
            Node node = (Node)path.list.get(path.list.size()-1);
            //若当前点没有被访问则获得当前点的索引并返回
            if(node.visit == 0)
            {
                result = i;
                break;
            }
        }
        return (Path)list.get(result);
    }
}

TestMaze类

package Maze;
public class TestMaze
{
    public static void main(String[] args)
    {
        Maze maze = new Maze();
        //初始化地图
        maze.init();
        MazeFrame frame = new MazeFrame(maze);
    }
}

结果演示

  • 开始程序,在网格中点击鼠标绘制地图,其中第一次点击为起点(绿色),第二次为终点(红色),之后的点击为墙壁不可以穿过(灰色)
  • 在文字框中输入需要储存地图数据的路径,如E://1.txt
  • 点击SaveMap

  • 点击Start启动寻路程序

  • 点击Restart重启程序
  • 在文本框中输入需要读取地图数据的路径
  • 点击LoadMap即可读取之前存储的地图信息