前言
在课堂上听老师讲了一个人工智能的基础小算法,感觉神奇而有趣,便自行研究一下。研究了几个小时,总算实现了。想写一篇博文来记录一下今天的学习经历,那么,就请有兴趣的各位跟我一起从零实现一个简单的A星寻路算法啦。
概述
- 首先,需要有个基本概念:A*搜寻算法俗称“A星算法”。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。(启发式搜索)
几个要素
- 是否相通的标记(标记有许多种方式,例如:0为通畅、1为障碍;或者把两两结点的线路保存下来,再去查该线路是否存在)
- 一个父亲结点指针(处理结点时候,需要知道该结点的父节点)
- OPEN 表(待检查的点,认为当前点为父亲结点,父亲结点也要保存,)
- CLOSE 表(不再检查的点)
- 一条公式:F = H + G
F 为到终点的总耗费
H 为网格上当前方格移动到终点的预估耗费(预估算法有很多种,例如:“曼哈顿法”:H = |行数| + |列数|、另外也可利用直线距离…);
G 为父亲结点移动到当前结点的预估移动耗费; 现在,再来举个栗子,下面的步骤解析也会与之相结合:
简单解释:
- 图中有 A、B、C、D、E、F、G、H 8个结点( A 为起点、E 为终点)
- 括号为当前结点到终点的预估值(由于预估算法的选择有许多,使用时需要根据实际情况选择,而我们现在优先学习A星算法的思路,因此这里直接给出预估值 - 即 H 值)
- 结点间的连线为相同直线,上面的数字代表线路的移动耗费值(即 G 值)
- 任务:利用A星算法计算出路线
算法步骤
- 设起点 A ,将 A 加入 OPEN 表
- 寻找相邻可达点,计算出其 F 值,并加入 OPEN 表;若相邻点已经在 Open 表中,则判断其是否需要更新 F 值;若相邻点已经在 Close 表中,则跳过该点
- 将原父亲结点(A 结点)从 OPEN 表中删除,把它加入到 CLOSE 表中
- 从 OPEN 表中选择一个 F值最小的,然后将它作为父亲结点,继续处理其相邻结点
- 只要 Open 表不为空,则循环执行上述2、3、4步骤,发现相邻点是终点,则跳出循环。
- 当 Open 表为空,还没有找到终点,则说明起点不可达到终点
Just show me the code !
讲了那么多,理论知识总该要实践才行,建议大家先参考伪代码,理解好算法思路,自行尝试实现一番,再去看我那些半桶水的实现代码,一定会有更多收获的。
一段核心思路伪代码:
while(OPEN!=NULL)
{
从OPEN表中取f(n)最小的节点n;
if(n节点==目标节点)
break;
for(当前节点n的每个子节点X)
{
计算f(X);
if(XinOPEN)
if(新的f(X)<OPEN中的f(X))
{
把n设置为X的父亲;
更新OPEN表中的f(n);
}
if(XinCLOSE)
continue;
if(Xnotinboth)
{
把n设置为X的父亲;
求f(X);
并将X插入OPEN表中;//还没有排序
}
}//endfor
将n节点插入CLOSE表中;
按照f(n)将OPEN表中的节点排序;//实际上是比较OPEN表内节点f的大小,从最小路径的节点向下进行。
}//endwhile(OPEN!=NULL)
我的 Java 实现代码:
package com.goo.astart;
/**
*路径实体类
*
* @author Goo
*
*/
public class Road {
public String name;
public int length;
public Road(String name, int length) {
this.name = name;
this.length = length;
}
}
package com.goo.astart;
/**
* 结点实体类
*
* @author Goo
*
*/
public class NodeStruct {
public NodeStruct(String nodeName, int estimate) {
this.nodeName = nodeName;
this.estimate = estimate;
}
/**
* 结点名称
*/
public String nodeName;
/**
* 父亲结点
*/
public NodeStruct fatherNode = null;
/**
* 总耗费
*/
public int weight = 0;
/**
* 当前结点到终点的预估耗费
*/
public int estimate = 0;
/**
* 父亲结点到当前结点的实际耗费
*/
public int practical = 0;
}
//Main.java
package com.goo.astart;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import javax.management.openmbean.OpenDataException;
import javax.xml.soap.Node;
/**
* 含有主方法的主逻辑类
*
* @author Goo
*
*/
public class Main {
private static ArrayList<NodeStruct> dataList = new ArrayList<NodeStruct>();
private static ArrayList<Road> roadList = new ArrayList<Road>();
private static ArrayList<NodeStruct> openList = new ArrayList<NodeStruct>();
private static ArrayList<NodeStruct> closeList = new ArrayList<NodeStruct>();
private static NodeStruct startNode;
private static NodeStruct endNode;
private static final int HASFOUND = 10000;
private static final int NOTFOUND = 20000;
private static int stepCount = 0;
public static void main(String[] args) {
loadData();
// 1、将起点加入Open表
addToOpenList(startNode);
// 2、搜索相邻路径?
if (search() == HASFOUND) {
System.out.println("路径搜索完成,成功找到最短路径");
} else {
System.out.println("路径搜索完成,没有可达路径");
}
}
/**
* A星核心算法
*
* @return
*/
private static int search() {
// Open表不为空
while (openList.size() != 0) {
// 按照总耗费从小到大排序
Collections.sort(openList, new Comparator<NodeStruct>() {
public int compare(NodeStruct node1, NodeStruct node2) {
return node1.weight - node2.weight;
};
});
// 取最小结点作为父亲结点
NodeStruct fatherNode = openList.get(0);
stepCount++;
System.out.println("步骤" + stepCount + ":" + fatherNode.nodeName
+ " - 结点F值:" + fatherNode.weight);
// 结点已找到
if (fatherNode.equals(endNode)) {
return HASFOUND;
}
// 寻找相邻路径
for (int i = 0; i < dataList.size(); i++) {
NodeStruct neighNode = dataList.get(i);
String roadName = neighNode.nodeName + fatherNode.nodeName;
String roadNameRev = fatherNode.nodeName + neighNode.nodeName;
for (int j = 0; j < roadList.size(); j++) {
Road tempRoad = roadList.get(j);
// 找到相邻点
if (roadName.equals(tempRoad.name)
|| roadNameRev.equals(tempRoad.name)) {
// 如果在Close表中,忽略
if (closeList.contains(neighNode)) {
continue;
}
else if (openList.contains(neighNode)) {
// 如果在Open表中
// 如果父亲结点到该结点耗费值更小,则更新耗费值
if (tempRoad.length + neighNode.estimate < neighNode.weight) {
neighNode.weight = tempRoad.length
+ neighNode.estimate;
}
} else {
// 两个表都没有(空白点)
// 计算该点总耗费
neighNode.weight = neighNode.estimate
+ tempRoad.length;
// 成功搜索路径,认为是相邻点,加入到Open表
addToOpenList(neighNode);
}
}
}
}
// 最后将父结点移除Open表,放入Close表
removeFromOpen(fatherNode);
addToCloseList(fatherNode);
}
return NOTFOUND;
}
/**
* 加载数据
*/
private static void loadData() {
startNode = new NodeStruct("A", 15);
startNode.weight = 15;
endNode = new NodeStruct("E", 0);
dataList.add(startNode);
dataList.add(endNode);
dataList.add(new NodeStruct("B", 14));
dataList.add(new NodeStruct("C", 10));
dataList.add(new NodeStruct("D", 2));
dataList.add(new NodeStruct("F", 5));
dataList.add(new NodeStruct("G", 9));
dataList.add(new NodeStruct("H", 11));
roadList.add(new Road("AB", 3));
roadList.add(new Road("AH", 4));
roadList.add(new Road("BH", 5));
roadList.add(new Road("CG", 3));
roadList.add(new Road("CD", 8));
roadList.add(new Road("CB", 4));
roadList.add(new Road("DG", 8));
roadList.add(new Road("DF", 3));
roadList.add(new Road("DE", 2));
roadList.add(new Road("FG", 4));
roadList.add(new Road("GH", 2));
}
/**
* 添加到 Open 表
*
* @param node
*/
private static void addToOpenList(NodeStruct node) {
openList.add(node);
}
/**
* 添加到 Close 表
*
* @param node
*/
private static void addToCloseList(NodeStruct node) {
closeList.add(node);
}
/**
* 从 Open 表移除
*
* @param node
*/
private static void removeFromOpen(NodeStruct node) {
openList.remove(node);
}
/**
* 从 Close 表移除
*
* @param node
*/
private static void removeFromClose(NodeStruct node) {
closeList.remove(node);
}
}
总结
相信大家动手实现后,就会对A*算法有个基本的认识啦,以上代码通过了几个简单的样例,但不排除会隐藏着奇怪的 Bug 噢,或者是一段有待改进的代码,欢迎大家指点指点。