前言
这个我也不知道算不算是A*搜索算法,可能只是A搜索算法。
首先看相关的定义:
启发式搜索在搜索过程中根据启发信息评估各个节点的重要性,优先搜索重要的节点。
估价函数的任务就是估计待搜索节点“有希望”的程度。
估价函数f(n)定义为从初始节点经过节点n到达目的节点的路径的最小代价估计值,其中一般形式是
f(n)=g(n)+h(n)
g(n)是从初始节点到节点n的实际代价,而h(n)是从节点n到目的节点的最佳路径的估计价值。
在这篇博客中就把事件进行简化了,g(n)表示当前走过的步数,h(n)估计代价用曼哈顿距离来表示。
参考链接:
人工智能大作业——A*算法迷宫寻路问题_a*算法实现迷宫寻路功能-CSDN博客https://blog.csdn.net/weixin_46037153/article/details/107136560A*搜索算法(A-Star Search)简介及保姆级代码解读 (qq.com)
https://mp.weixin.qq.com/s?__biz=MzU1NjEwMTY0Mw==&mid=2247554483&idx=1&sn=c3e6a3259f1b469eded07a5498790186&chksm=fbc86cd7ccbfe5c12f4dddf139f73cb5c2879044867bced71edb0243b155f3d68eda50d0db6a&scene=27机器人路径规划算法(十一)A-star算法 - Mronne's Blog
https://mronne.github.io/2020/04/03/%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%B7%AF%E5%BE%84%E8%A7%84%E5%88%92%E7%AE%97%E6%B3%95-%E5%8D%81%E4%B8%80-A-star-%E7%AE%97%E6%B3%95.html
思路讲解
前期准备
用MAP二维数组来表示待搜索的地图,其中1表示道路受阻碍,0表示通路。
定义Grid来表示地图中的节点,其中x,y表示坐标,fn,hn,gn是之后需要用到的估计值。
定义两个集合来储存新加入的节点以及走过的节点,我这里用到的是链表,因为添加和删除元素比较多。
//定义地图,直接利用二维数组来实现(0表示可以通过,1表示不能通过
public static int[][] MAP = {
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 1, 1, 1, 1},
{0, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0},
};
//把方格抽象成一个类
public static class Grid {
private int x;//横坐标
private int y;//纵坐标
//fn=hn+gn
int fn;//估计函数
int hn;//估计代价
int gn;//实际代价
private Grid present;//当前节点
//构造方法
public Grid(int x, int y) {
this.x = x;
this.y = y;
}
//实例化一个方格节点
public void initGrid(Grid present, Grid end) {
this.present = present;
//计算gn
if (present != null) {
//实际的代价加一相当于前进了一步
this.gn = present.gn + 1;
} else {
this.gn = 1;
}
//计算hn的大小(这里用的估计代价是曼哈顿距离
this.hn = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
//计算fn的大小
this.fn = this.gn + this.hn;
}
public String toString() {
return "(" + this.x + "," + this.y + ")" + "fn:" + this.fn + " gn:" + this.gn + " hn:" + this.hn;
}
@Override
public int hashCode() {
int tmp = (this.y + (this.x + 1) / 2);
return x + (tmp * tmp);
}
//重写equals方法,不然比较的是地址值
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Grid other = (Grid) obj;
if (this.x == other.x && this.y == other.y) {
return true;
}
return false;
}
}
//准备两个链表来储存需要选择的节点以及已经走过的节点
public static LinkedList<Grid> selectList = new LinkedList<Grid>();
public static LinkedList<Grid> walkList = new LinkedList<Grid>();
public static HashMap<Grid, Grid> resultMap = new HashMap<Grid, Grid>();
查找最小估价值的节点
方法的作用是返回selectList集合中的fn最小的节点。(fn表示最小代价估计值)
//返回最小的那个节点
private static Grid findMinGrid(LinkedList<Grid> selectList) {
Grid tmpgrid = selectList.get(0);
for (Grid grid : selectList) {
//更新最小节点
if (grid.fn < tmpgrid.fn) {
tmpgrid = grid;
}
}
return tmpgrid;
}
判断当前节点是否已经添加
判断(x,y)坐标是否在grids集合中,如果相等,表示已经添加到集合中了。
//判断当前的节点是否已经添加
private static boolean contains(LinkedList<Grid> grids, int x, int y) {
for (Grid grid : grids) {
//坐标在grids集合中有就是已经被添加了
if ((grid.x == x) && (grid.y == y)) {
return true;
}
}
return false;
}
判断当前的节点是否合法
判断当前节点的坐标是否越界,以及节点是否已经被添加到集合中了,如果已经被添加了就不用管了。
//判断当前节点是否合法
private static boolean legitimacy(int x, int y, LinkedList<Grid> selectList, LinkedList<Grid> walkList) {
//判断坐标是否越界
if (x < 0 || x >= MAP.length || y < 0 || y >= MAP[0].length) {
return false;
}
//判断当前节点是否是障碍
if (MAP[x][y] == 1) {
return false;
}
//判断当前节点是否被添加
if (contains(selectList, x, y)) {
return false;
}
//判断当前节点是否已经走过了
if (contains(walkList, x, y)) {
return false;
}
//所以条件都满足,他就是合法的
return true;
}
添加相邻的节点
他的思路就是先找出目前fn最小的那个节点,然后对那个节点相邻的节点进行判断,如果相邻的节点是合法的,就把节点添加到集合中。
//把所有符合要求的相邻节点都放置到list集合里面里面
private static LinkedList<Grid> findNeighbors(Grid grid, LinkedList<Grid> selectList, LinkedList<Grid> walkList) {
LinkedList<Grid> list = new LinkedList<Grid>();
//判断相邻节点的合法性
if (legitimacy(grid.x, grid.y - 1, selectList, walkList)) {//下(用数组来看的话他就是往上)
list.add(new Grid(grid.x, grid.y - 1));
}
if (legitimacy(grid.x, grid.y + 1, selectList, walkList)) {//上
list.add(new Grid(grid.x, grid.y + 1));
}
if (legitimacy(grid.x - 1, grid.y, selectList, walkList)) {//左
list.add(new Grid(grid.x - 1, grid.y));
}
if (legitimacy(grid.x + 1, grid.y, selectList, walkList)) {//右
list.add(new Grid(grid.x + 1, grid.y));
}
return list;
}
算法实现
先把start节点最开始添加到集合中,然后不断的扩展他,知道集合中没有节点,或者找到终点节点表示结束。用当前节点作为值,邻居节点作为键加入到HashMap中,HashMap和TreeMap的储存结构不同,我用TreeMap来储存结果导致键和值都有,直接用keySet()和values()他是有的,但是用containsKey()以及get()传入键返回的是null,这个可能和他们的储存方式不同有关,这里我研究了半天也不知道是什么原因。
//A*算法的实现(需要开始位置和结束位置
private static void algorithmImplementation(Grid start, Grid end) {
//将起点加入链表之后开始寻路
selectList.add(start);
//链表不为空(没有最后没有到达终点也会停止)
while (selectList.size() > 0) {
//找到在需要选择的节点中最小的那个节点,之后需要用它进行扩展
Grid nowGrid = findMinGrid(selectList);
//从中删除最小的那个节点
selectList.remove(nowGrid);
//将这个节点添加到已经走过的路径中
walkList.add(nowGrid);
//寻找他的相邻节点,把合法的节点都添加进来
LinkedList<Grid> neighbors = findNeighbors(nowGrid, selectList, walkList);
for (Grid grid : neighbors) {
//判断集合中是否添加了grid节点
if (!selectList.contains(grid)) {
//进行初始化
grid.initGrid(nowGrid, end);
//添加到待搜索集合中
selectList.add(grid);
if (grid != null && nowGrid != null) {
//子节点作为键,父节点作为值
resultMap.put(grid, nowGrid);
}
//System.out.println(grid + " " + resultMap.get(grid));
}
}
//判断是否可以结束
for (Grid grid : selectList) {
if ((grid.x == end.x) && (grid.y == end.y)) {
walkList.add(end);
return;
}
}
}
}
主方法
对储存的节点进行打印,走过的道路用2来表示,最终的路径用3来表示。
//主方法
public static void main(String[] args) {
System.out.println("原始地图是:");
for (int i = 0; i < MAP.length; i++) {
for (int j = 0; j < MAP[0].length; j++) {
System.out.print(MAP[i][j] + "\t");
}
System.out.println();
}
Grid grid1 = new Grid(0, 0);
Grid grid2 = new Grid(MAP.length - 1, MAP[0].length - 1);
//调用A*搜索算法
algorithmImplementation(grid1, grid2);
//走过的路径用2表示
for (Grid grid : walkList) {
MAP[grid.x][grid.y] = 2;
}
while (true) {
MAP[grid2.x][grid2.y] = 3;
Grid grid = resultMap.get(grid2);
grid2 = grid;
if (grid2 == null) {
break;
}
//到达起点
if (grid2.x == grid1.x && grid2.y == grid1.y) {
MAP[grid2.x][grid2.y]=3;
break;
}
}
System.out.println("搜索后的地图是:");
for (int i = 0; i < MAP.length; i++) {
for (int j = 0; j < MAP[0].length; j++) {
System.out.print(MAP[i][j] + "\t");
}
System.out.println();
}
}
Java
import java.util.*;
public class AStarSearch {
//主方法
public static void main(String[] args) {
System.out.println("原始地图是:");
for (int i = 0; i < MAP.length; i++) {
for (int j = 0; j < MAP[0].length; j++) {
System.out.print(MAP[i][j] + "\t");
}
System.out.println();
}
Grid grid1 = new Grid(0, 0);
Grid grid2 = new Grid(MAP.length - 1, MAP[0].length - 1);
//调用A*搜索算法
algorithmImplementation(grid1, grid2);
//走过的路径用2表示
for (Grid grid : walkList) {
MAP[grid.x][grid.y] = 2;
}
/*for (Grid grid : selectList) {
MAP[grid.x][grid.y] = 4;
}*/
while (true) {
MAP[grid2.x][grid2.y] = 3;
Grid grid = resultMap.get(grid2);
grid2 = grid;
if (grid2 == null) {
break;
}
//到达起点
if (grid2.x == grid1.x && grid2.y == grid1.y) {
MAP[grid2.x][grid2.y]=3;
break;
}
}
/*for(Iterator<Grid>it=resultMap.keySet().iterator();it.hasNext();){
Grid grid=resultMap.get(it.next());
if(grid!=null){
System.out.println(grid);
}
//MAP[grid.x][grid.y]=3;
}*/
//System.out.println(resultMap.keySet());
//System.out.println(resultMap.values());
/* Grid grid = new Grid(1, 0);
grid.fn = 6;
grid.gn = 1;
grid.hn = 5;
if (resultMap.keySet().contains(grid)) {
System.out.println("有");
} else {
System.out.println("没有");
}
if(resultMap.containsKey(grid)){
System.out.println("有");
}else {
System.out.println("没有");
}*/
/* if(grid.equals(new Grid(1,0))){
System.out.println("相等");
}else {
System.out.println("不相等");
}
System.out.println(grid);
System.out.println(resultMap.get(grid));
*/
System.out.println("搜索后的地图是:");
for (int i = 0; i < MAP.length; i++) {
for (int j = 0; j < MAP[0].length; j++) {
System.out.print(MAP[i][j] + "\t");
}
System.out.println();
}
}
//定义地图,直接利用二维数组来实现(0表示可以通过,1表示不能通过
public static int[][] MAP = {
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 1, 1, 1, 1},
{0, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0},
};
//把方格抽象成一个类
public static class Grid {
private int x;//横坐标
private int y;//纵坐标
//fn=hn+gn
int fn;//估计函数
int hn;//估计代价
int gn;//实际代价
private Grid present;//当前节点
//构造方法
public Grid(int x, int y) {
this.x = x;
this.y = y;
}
//实例化一个方格节点
public void initGrid(Grid present, Grid end) {
this.present = present;
//计算gn
if (present != null) {
//实际的代价加一相当于前进了一步
this.gn = present.gn + 1;
} else {
this.gn = 1;
}
//计算hn的大小(这里用的估计代价是曼哈顿距离
this.hn = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
//计算fn的大小
this.fn = this.gn + this.hn;
}
public String toString() {
return "(" + this.x + "," + this.y + ")" + "fn:" + this.fn + " gn:" + this.gn + " hn:" + this.hn;
}
@Override
public int hashCode() {
int tmp = (this.y + (this.x + 1) / 2);
return x + (tmp * tmp);
}
//重写equals方法,不然比较的是地址值
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Grid other = (Grid) obj;
if (this.x == other.x && this.y == other.y) {
return true;
}
return false;
}
}
/* //对Grid类进行判断
static class GridComparator implements Comparator<Grid> {
@Override
public int compare(Grid grid1, Grid grid2) {
if (grid1.x == grid2.x && grid1.y == grid2.y) {
return 0;
} else {
return 1;
}
}
}*/
//准备两个链表来储存需要选择的节点以及已经走过的节点
public static LinkedList<Grid> selectList = new LinkedList<Grid>();
public static LinkedList<Grid> walkList = new LinkedList<Grid>();
public static HashMap<Grid, Grid> resultMap = new HashMap<Grid, Grid>();
/* public static TreeMap<Grid, Grid> resultMap = new TreeMap<Grid, Grid>(new Comparator<Grid>() {
@Override
public int compare(Grid o1, Grid o2) {
//用坐标标识就行了
if (o1.x == o2.x && o1.y == o2.y) {
return 0;
} else {
return 1;
}
}
});*/
//A*算法的实现(需要开始位置和结束位置
private static void algorithmImplementation(Grid start, Grid end) {
//将起点加入链表之后开始寻路
selectList.add(start);
//链表不为空(没有最后没有到达终点也会停止)
while (selectList.size() > 0) {
//找到在需要选择的节点中最小的那个节点,之后需要用它进行扩展
Grid nowGrid = findMinGrid(selectList);
//从中删除最小的那个节点
selectList.remove(nowGrid);
//将这个节点添加到已经走过的路径中
walkList.add(nowGrid);
//寻找他的相邻节点,把合法的节点都添加进来
LinkedList<Grid> neighbors = findNeighbors(nowGrid, selectList, walkList);
for (Grid grid : neighbors) {
//判断集合中是否添加了grid节点
if (!selectList.contains(grid)) {
//进行初始化
grid.initGrid(nowGrid, end);
//添加到待搜索集合中
selectList.add(grid);
if (grid != null && nowGrid != null) {
//子节点作为键,父节点作为值
resultMap.put(grid, nowGrid);
}
//System.out.println(grid + " " + resultMap.get(grid));
}
}
//判断是否可以结束
for (Grid grid : selectList) {
if ((grid.x == end.x) && (grid.y == end.y)) {
walkList.add(end);
return;
}
}
}
}
//把所有符合要求的相邻节点都放置到list集合里面里面
private static LinkedList<Grid> findNeighbors(Grid grid, LinkedList<Grid> selectList, LinkedList<Grid> walkList) {
LinkedList<Grid> list = new LinkedList<Grid>();
//判断相邻节点的合法性
if (legitimacy(grid.x, grid.y - 1, selectList, walkList)) {//下(用数组来看的话他就是往上)
list.add(new Grid(grid.x, grid.y - 1));
}
if (legitimacy(grid.x, grid.y + 1, selectList, walkList)) {//上
list.add(new Grid(grid.x, grid.y + 1));
}
if (legitimacy(grid.x - 1, grid.y, selectList, walkList)) {//左
list.add(new Grid(grid.x - 1, grid.y));
}
if (legitimacy(grid.x + 1, grid.y, selectList, walkList)) {//右
list.add(new Grid(grid.x + 1, grid.y));
}
return list;
}
//判断当前节点是否合法
private static boolean legitimacy(int x, int y, LinkedList<Grid> selectList, LinkedList<Grid> walkList) {
//判断坐标是否越界
if (x < 0 || x >= MAP.length || y < 0 || y >= MAP[0].length) {
return false;
}
//判断当前节点是否是障碍
if (MAP[x][y] == 1) {
return false;
}
//判断当前节点是否被添加
if (contains(selectList, x, y)) {
return false;
}
//判断当前节点是否已经走过了
if (contains(walkList, x, y)) {
return false;
}
//所以条件都满足,他就是合法的
return true;
}
//判断当前的节点是否已经添加
private static boolean contains(LinkedList<Grid> grids, int x, int y) {
for (Grid grid : grids) {
//坐标在grids集合中有就是已经被添加了
if ((grid.x == x) && (grid.y == y)) {
return true;
}
}
return false;
}
//返回最小的那个节点
private static Grid findMinGrid(LinkedList<Grid> selectList) {
Grid tmpgrid = selectList.get(0);
for (Grid grid : selectList) {
//更新最小节点
if (grid.fn < tmpgrid.fn) {
tmpgrid = grid;
}
}
return tmpgrid;
}
}
总结
现在只写了Java的代码,之后会有时间会用其他语言写,之前问了老师,他说用Python可以实现图形化的那种,但是面前还不太会,以后有时间就会更新一下这篇博客。
9月27日补充,这个算是A*搜索算法,因为他这个迷宫的走法就是上下左右,不能够斜着走,所以曼哈顿距离就是最小的代价。但是目前的缺点是图太简单了,而且没有实现可视化,也没有把最短的路径标记出来,只是把走过的路标记了起来,所以还没有达到目标的要求,这个问了老师说需要用到回溯的思想,但是目前还没实现。