它有啥用?
没错!这就是那个传说中的算法,AStar算法。
直白的描述就是,它能告诉你从源点绕过障碍物到达终点的最短路径。
以魔兽争霸为例,你控制一个游戏单位从上次鼠标右键点击的位置到本次鼠标右键点击的位置走的路线,中间可能有商店、水泉、树木、高坡等障碍物。
再举个例子,不过这个例子是我估计的,我估计刀塔里面英雄的走位操作也是用这个算法实现的。
它的原理是什么?
很简单,广度优先搜索+迪杰斯特拉算法。
性能如何?
很快,不过在我看来还不够快,因为广度优先嘛,像震荡波一样向周围辐射,所以那些本来不是路径的地方也会被遍历一遍,不完美,不过相对来说还是很快的。空间嘛,太浪费空间了,而且路线精度要求越高越消耗空间。
路径形成的关键是什么?
链表和指针。
它把每个涉及到的顶点都用结构体或者对象封装成了链表结点。
每出队一个顶点就让它周围的顶点指向它,以此类推,最终终点变成了链表头,起点变成了链表尾。
我是感觉这个设计挺好值得借鉴。
至于其他的要义可以百度出来。
具体是什么效果?
请看下图。
A星算法
代码的用法
寻找路径的范围往往被抽象为一个二维数组,或者说地图被抽象为一个二维数组也行。拿地图来说事吧,都玩过《红色警戒》吧,那里面地图的组成小单位是小方块,这个很典型,其他形状的也有,比如说正六边形等。在A*算法中这些小形状都被抽象成为它的中心点,所以无论是普通点、终点、起点还是障碍点都是点。
在本算法中
0代表可走点。
-1代表障碍点。
1代表起点。
2代表终点。
3代表已经找到的路径点。
public class Main {
public static void main(String[] args) {
// write your code here
//这里规定障碍结点为-1.
// 起点值为1,
// 终点值为2,
// 其他点为0.
int[][] testMatrix = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,-1,0,0,0,0,0,0,0,0,0},
{1,0,0,0,-1,0,0,0,0,0,0,0,0,0},
{0,0,0,0,-1,0,0,0,0,0,0,0,0,0},
{0,0,0,0,-1,0,0,-1,0,0,0,0,0,0},
{0,0,0,0,-1,0,0,-1,0,0,0,0,0,0},
{0,0,0,0,-1,0,0,-1,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,-1,0,0,0,0,0},
{0,0,0,0,0,0,0,0,-1,2,0,0,0,0},
{0,0,0,0,0,0,0,0,-1,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
};
int[][] testMatrix0 = {
{0,0,0,0,0,0,0},
{0,0,0,-1,0,0,0},
{0,1,0,-1,0,2,0},
{0,0,0,-1,0,0,0},
{0,0,0,0,0,0,0}
};
AStar.aStar0(testMatrix,2,0,8,9);
for (int counter = 0;counter < testMatrix.length;counter++) {
for (int counter0 = 0;counter0 < testMatrix[0].length;counter0++) {
System.out.print(testMatrix[counter][counter0] + " ");
}
System.out.println();
}
}
}
输出
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 -1 0 0 0 0 0 0 0 0 0
1 0 0 0 -1 0 0 0 0 0 0 0 0 0
0 3 0 0 -1 0 0 0 0 0 0 0 0 0
0 0 3 0 -1 0 0 -1 0 0 0 0 0 0
0 0 0 3 -1 0 0 -1 0 0 0 0 0 0
0 0 0 3 -1 0 0 -1 0 0 0 0 0 0
0 0 0 0 3 0 0 0 -1 0 0 0 0 0
0 0 0 0 0 3 3 0 -1 2 0 0 0 0
0 0 0 0 0 0 0 3 -1 3 0 0 0 0
0 0 0 0 0 0 0 0 3 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
/**
* A*算法是用来干嘛的?
* 它是用来寻找从源点到终点的最小的可能路径。
* 什么叫可能路径?
* 形象一点,比方说你要去目的地,沿途有一道峡谷,又宽又深,
* 又没有桥有没有飞机,也没有阿凡达,你又不会飞。
* 那咋办?那你只能绕过去了,所以这个峡谷就是不
* 可能路径的范畴了。
* 再比如,在你的路径上面有一堵墙,这墙又高又厚没有门窗。
* 你咋过?你会穿墙术?你当然不会,于是这也在不可能路径的范畴内。
* 那咋办?那你只能在没有障碍物的路径上才能到达目的地了。
* 那么怎么在没有障碍物的路径上怎么走路程最小呢?
* 那就是本算法要解决的问题了。
* 一般把这个所有的路径,包括可行不可行的,以及起点和终点
* 都抽象在一个二维平面上。
*/
public class AStar {
/**
* 从源点到终点的可行路径有很多,但是最短路径一般只有一条。
* 在试探的过程中,每个顶点的相邻顶点有8个,我想这一点很明确。
* 如果不明白可以去玩玩扫雷。
* 于是每个顶点有8个方向可以移动,此时这个顶点被称为父结点。
* 而你所需要做的就是在这8个方向中,排除不能走的方向,找到最佳的顶点。
* 然后再以这个最佳的顶点为父结点开始找它自身的8个方向中的最佳顶点。
* 那么什么是最佳顶点呢?
* 其实本算法的精髓就在这里,这个最佳点是在父结点的8个结点中选出父结点
* 到该结点的路程+该结点到终点的路程之和最小的那个结点,这个结点
* 就是所谓的最佳结点。
* 网上通常把这个比较称为f(n)=g(n)+h(n)。
* n就是当前父结点。
* f就是和
* g就是从当前结点到起点的路程。
* h就是该结点到终点的路程,其实这代表的是该结点与终点的距离,或者说接近程度。
* 不过有的时候最小的f(n)有好几个相等的,这个不用管,算法会自动找一个。
* 那么如何获取最佳路径呢?
* 只需要把每个父结点存储起来就行。
* 我搞错了,原来这个g值是结点当前的g值加上它父结点的g值得到的。
* 如何高效地确定某结点的周围结点已经在开放列表中?
* 用数组的话效率会非常低,因为你肯定要遍历,但是如
* 果你用哈希表的话需要的空间开销特大,尤其是如果地
* 图被划分得比较细的话。
* 我认为把周围的顶点变成结点,并用标志位把是否在开
* 放列表中指示出来。
* 没办法,看来只能需要一个辅助空间来换取时间上的高效了。
* 本算法其实用不着open表,因为已经使用二维数组作为索引
* ,所以没有必要再判断该结点在不在open表中了,因为但凡
* 被加入到open表中的结点肯定是新结点,与此相同新结点在
* 使用之前也一定先被加入到索引数组中。
* 这个结点包含的信息是一个比较复杂的结构,所以每个结点
* 适合用对象进行封装。
* A*算法的关键在于当前结点的周围结点可能和别的已经被淘
* 汰的结点尤其是前一个结点的周围结点重合。在算法中表现
* 为新结点周围的结点有一部分已经在open表中了。对于重叠
* 的结点以当前结点作为中间结点可能存在更优的路径,具体
* 就是指g值更小。比较的方法是假设现在比较的是当前结点的
* 周围结点A,经由当前结点到达A的时候,A的g值比原来的g
* 值更小。这就代表更优解出现了于是需要改变当前结点的父指
* 针到当前结点的父指针的父指针。这种情况常见于当前结点和
* 它的父结点以及父父结点都邻接。
* 排序最好使用小根堆排序,首先是因为它不需要额外空间,其
* 次它的复杂度是O(nlogn)比较快。但是它对下标有要求,如
* 果不想调整下标的话可以使用快排来调整。
* @param sourceArray
* @param sourcePointX
* @param sourcePointY
* @param targetPointX
* @param targetPointY
*/
static public void aStar0(int[][] sourceArray,
int sourcePointX,
int sourcePointY,
int targetPointX,
int targetPointY) {
//首先检查输入是否合法。
// 起点和终点不能越界
//获取xy维度。
int xLength = sourceArray.length;
int yLength = sourceArray[0].length;
if (sourcePointX < 0 || sourcePointX >= xLength || targetPointY < 0 || targetPointY >= yLength) {
System.out.println("起点或终点输入越界");
return;
}
//其次不能和障碍物重叠
if (sourceArray[sourcePointX][sourcePointY] == -1 || sourceArray[targetPointX][targetPointY] == -1) {
System.out.println("与障碍点冲突");
return;
}
//起点和终点不能相等
if (sourcePointX == targetPointX && sourcePointY == targetPointY) {
System.out.println("起点和终点相同");
return;
}
//为了把索引的结点的时间加快到常数级别,在这里需要一个和源数组同等维度的二维数组。
VertexObject[][] vertexIndexTable = new VertexObject[xLength][yLength];
//正向行走是10
final int obstacle = 10;
//斜向行走是14
final int obliqueObstacle = 14;
//首先把起点和终点放到索引数组里面去。
vertexIndexTable[sourcePointX][sourcePointY] = new VertexObject(sourcePointX,sourcePointY);
vertexIndexTable[sourcePointX][sourcePointY].setPointType(VertexObject.PointType.startPoint);
vertexIndexTable[sourcePointX][sourcePointY].setG(0);
vertexIndexTable[sourcePointX][sourcePointY].setH((Math.abs(targetPointX - sourcePointX) + Math.abs(targetPointY - sourcePointY)) * obstacle);
vertexIndexTable[targetPointX][targetPointY] = new VertexObject(targetPointX,targetPointY);
vertexIndexTable[targetPointX][targetPointY].setPointType(VertexObject.PointType.endPoint);
vertexIndexTable[targetPointX][targetPointY].setH(0);
//现在需要一个数组,其实它就是open表。
// 我也不知道多长合适,于是来个最长的吧,反正现在电脑内存够用。
int openTableLength = xLength * yLength;
VertexObject[] openTable = new VertexObject[openTableLength];
//其实这就是个队列,但是这个队列用不着循环队列。
int front = 0;
int rear = 0;
//先把起始点放到open队列中。
openTable[rear++] = vertexIndexTable[sourcePointX][sourcePointY];
//方向数组,从西北顺时针旋转。
final int[][] vectorArray = {
{-1,-1},{0,-1},{1,-1},{1,0},
{1,1},{0,1},{-1,1},{-1,0}
};
//循环终止的条件是要么open表为空或者终点进入到open表中。
while (front < rear && openTable[rear - 1].getPointType() != VertexObject.PointType.endPoint) {
AStar.sortByHeap(openTable,front,rear - front);
VertexObject currentObject = openTable[front++];
currentObject.setPointState(VertexObject.StateType.closedPoint);
//把它的8个邻接点一个一个地送入队列。
for (int counter = 0;counter < 8;counter++) {
int adjacentPointX = currentObject.getCoordinateX() + vectorArray[counter][1];
int adjacentPointY = currentObject.getCoordinateY() + vectorArray[counter][0];
//如果越界就跳过
if (adjacentPointX < 0
|| adjacentPointX >= xLength
|| adjacentPointY <0
|| adjacentPointY >= yLength)
continue;
//如果是障碍物就跳过
if (sourceArray[adjacentPointX][adjacentPointY] == -1)
continue;
//如果该结点已经在close表中了
if (vertexIndexTable[adjacentPointX][adjacentPointY] != null
&& vertexIndexTable[adjacentPointX][adjacentPointY].getPointState()
== VertexObject.StateType.closedPoint)
continue;
//如果是终点那就结束循环因为此时已经找到了路径,因为此时是终点。
if (adjacentPointX == targetPointX
&& adjacentPointY == targetPointY) {
vertexIndexTable[targetPointX][targetPointY].setPrePointer(currentObject);
openTable[rear++] = vertexIndexTable[targetPointX][targetPointY];
break;
}
if (vertexIndexTable[adjacentPointX][adjacentPointY] == null) {
VertexObject newNode = new VertexObject(adjacentPointX,adjacentPointY);
vertexIndexTable[adjacentPointX][adjacentPointY] = newNode;
int orginalG = currentObject.getG();
int newG = (vectorArray[counter][0] * vectorArray[counter][1] == 0)?
(orginalG + obstacle):(orginalG + obliqueObstacle);
newNode.setG(newG);
int hLength = (Math.abs(targetPointX - adjacentPointX) +
Math.abs(targetPointY - adjacentPointY)) * obstacle;
newNode.setH(hLength);
newNode.setPrePointer(currentObject);
openTable[rear++] = newNode;
} else {
//剩下的就是正常的情况了。如果这个点已经在open表中的话
int orginalG = vertexIndexTable[adjacentPointX][adjacentPointY].getG();
int newG = (vectorArray[counter][0] * vectorArray[counter][1] == 0)?
(orginalG + obstacle):(orginalG + obliqueObstacle);
if (newG < orginalG) {
vertexIndexTable[currentObject.getCoordinateX()]
[currentObject.getCoordinateY()].setPrePointer(currentObject.getPrePointer());
vertexIndexTable[currentObject.getCoordinateX()]
[currentObject.getCoordinateY()].setG(newG);
AStar.sortByHeap(openTable,front,rear - front);
}
}
}
}
//对原数组进行赋值
if (front < rear) {
VertexObject currentPointer = vertexIndexTable[targetPointX][targetPointY];
while (currentPointer != null) {
if (currentPointer.getPointType() == VertexObject.PointType.startPoint)
sourceArray[currentPointer.getCoordinateX()][currentPointer.getCoordinateY()] = 1;
else if (currentPointer.getPointType() == VertexObject.PointType.endPoint)
sourceArray[currentPointer.getCoordinateX()][currentPointer.getCoordinateY()] = 2;
else sourceArray[currentPointer.getCoordinateX()][currentPointer.getCoordinateY()] = 3;
currentPointer = currentPointer.getPrePointer();
}
}
}
/**
* 小顶堆排序,堆排序只能从0开始,要不然不正确。
* @param openTable
* @param startIndex
* @param sortLength
*/
static private void sortByHeap(VertexObject[] openTable,int startIndex,int sortLength) {
VertexObject[] tempArray = new VertexObject[sortLength];
for (int counter = 0,counter0 = startIndex;counter < sortLength;counter++,counter0++)
tempArray[counter] = openTable[counter0];
for (int counter = sortLength / 2 - 1;counter > -1;counter--)
adjustToSmallHeap(tempArray,counter,sortLength);
for (int counter = 0,counter0 = startIndex;counter < sortLength;counter++,counter0++)
openTable[counter0] = tempArray[counter];
}
/**
* 堆调整
* @param sourceArray
* @param rootIndex
* @param adjustLength
*/
static private void adjustToSmallHeap(VertexObject[] sourceArray,int rootIndex,int adjustLength) {
for (int counter = rootIndex;counter < adjustLength / 2;) {
int leftChildIndex = 2 * counter + 1;
int rightChildIndex = leftChildIndex + 1;
int smallerPointer = leftChildIndex;
if (rightChildIndex < adjustLength &&
sourceArray[leftChildIndex].getF() >
sourceArray[rightChildIndex].getF())
smallerPointer = rightChildIndex;
if (sourceArray[counter].getF() > sourceArray[smallerPointer].getF()) {
VertexObject tempElement = sourceArray[counter];
sourceArray[counter] = sourceArray[smallerPointer];
sourceArray[smallerPointer] = tempElement;
counter = smallerPointer;
} else break;
}
}
}
public class VertexObject {
public enum PointType{startPoint,endPoint,reachablePoint};
public enum StateType{availablePoint,closedPoint};
//这个就是A*算法中说的父结点
private VertexObject prePointer = null;
private int coordinateX = -1;
private int coordinateY = -1;
private StateType pointState = StateType.availablePoint;
private PointType pointType = PointType.reachablePoint;
private int g = 0;
private int h = 0;
private int f = 0;
public VertexObject(int coordinateX, int coordinateY) {
this.coordinateX = coordinateX;
this.coordinateY = coordinateY;
}
public StateType getPointState() {
return pointState;
}
public void setPointState(StateType pointState) {
this.pointState = pointState;
}
public int getCoordinateX() {
return coordinateX;
}
public int getCoordinateY() {
return coordinateY;
}
public int getG() {
return g;
}
public void setG(int g) {
this.g = g;
f = g + h;
}
public void setH(int h) {
this.h = h;
f = g + h;
}
public int getF() {
f = g + h;
return f;
}
public void setPointType(PointType pointType) {
this.pointType = pointType;
}
public PointType getPointType() {
return pointType;
}
public VertexObject getPrePointer() {
return prePointer;
}
public void setPrePointer(VertexObject prePointer) {
this.prePointer = prePointer;
}
}