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(),就是判断节点是否在关闭列表和开启列表内用的。