營救公主(Java實現A*演算法解決迷宮問題)
很早就聽說過A*演算法,據說在尋路徑時,是一種比較高效的演算法。但是一直沒有搞清楚原理。
這段時間剛好有個營救公主的例子:
題描述 :
公主被魔王抓走了 , 王子需要拯救出美麗的公主 。 他進入了魔王的城
堡 , 魔王的城堡是一座很大的迷宮 。 為了使問題簡單化 , 我們假設這個迷宮是一
個 N*M 的二維方格 。 迷宮裡有一些牆 , 王子不能通過 。 王子只能移動到相鄰 ( 上
下左右四個方向 ) 的方格內 , 並且一秒只能移動一步 , 就是說 , 如果王子在 (x,y )
一步只能移動到 (x-1,y),(x+1,y),(x,y-1),(x,y+1) 其中的一個位置上。地圖由
‘S’,‘P’,‘ . ’ , ‘ *’ 四種符號構成 , ‘ . ’ 表示王子可以通過 , ‘ *’ 表示
牆,王子不能通過;'S'表示王子的位置;‘P’表示公主的位置; n表示公主存活的剩餘時間,王子必須在 n 秒
內到達公主的位置,才能救活公主。
解題思路:
1、可以通過廣度優先的演算法進行演進,不停的查詢王子的所有下一點的位置,沒查詢一次時間減1,直到找到了公主或者時間為0時終止。
這個演算法能夠比較快速的解決上述的迷宮問題;
2、通過A*演算法,查找出每次移動可能到達的所有點,並設定了一定的權值規則,每次選取權值最小的一個點找它的下一個點……(當然,這是搞懂原理之後的後話:) )
本著鍛鍊下自己的原則選擇了A*演算法解決上面的問題。
原理我就不班門弄斧了,詳細請看牛人的博文:http://www.blueidea.com/tech/multimedia/2005/3121_3.asp,下面的回覆中還有個牛人實現了個Flash版的A*演算法。我個人比較笨,看了好幾遍才明白意思。各位如果沒接觸過且想學的,不妨在紙上或者電腦上按照圖示演算一遍,相信很快就能搞清楚原理:)
程式碼實現簡要說明:
1、定義了一個迷宮類 Maze,迷宮中包含了王子Prince(包含核心演算法)和迷宮的地圖MazeMap,迷宮(遊戲)啟動時,會先初始化地圖,然後王子開始尋路(具體演算法看程式碼);
2、定義了一個位置類Position,描述了二維座標資訊,及加權的PositionWeight類,包含了位置資訊、距離王子的距離(A*演算法中的G)、距離公主的距離(A*演算法中的H)、及二者的總開銷(F=G+H);
相信看懂了A*演算法的原理的朋友,很快就能寫出一個迷宮的實現方案。
下面貼一下我的實現,註釋還算比較詳盡,歡迎批評指正:)
/** * 迷宮中的位置點 建立人:dobuy * */ public class Position { /** * 水平或者垂直移動一格的距離 */ private final static int STRAIGHT_DISTANCE = 10; /** * 對角線移動一格的距離 */ private final static int DIAGONAL_LINE_DISTANCE = 14; private int x; private int y; public Position(int x, int y) { super(); this.x = x; this.y = y; } /** * 獲取從父節點直接偏移至子節點的距離 */ public int getOffsetOfDistance(Position position) { int x = Math.abs(getX() - position.getX()); int y = Math.abs(getY() - position.getY()); Position offset = new Position(x, y); if (offset.equals(new Position(0, 1)) || offset.equals(new Position(1, 0))) { return STRAIGHT_DISTANCE; } return DIAGONAL_LINE_DISTANCE; } /** * 獲取到目標節點的平移距離 */ public int getDistanceOfTarget(Position position) { int verticalDistance = Math.abs(getY() - position.getY()); int horizontalDistance = Math.abs(getX() - position.getX()); return (verticalDistance + horizontalDistance) * STRAIGHT_DISTANCE; } public Position offset(Position offset) { return new Position(getX() + offset.getX(), getY() + offset.getY()); } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; result = prime * result + y; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Position other = (Position) obj; if (x != other.x) return false; if (y != other.y) return false; return true; } @Override public String toString() { return "Position [x=" + x + ", y=" + y + "]"; } }
/**
* 位置資訊的權值
*/
public class PositionWeight
{
/**
* 水平或者垂直移動一格的距離
*/
private final static int STRAIGHT_DISTANCE = 10;
/**
* 當前的位置資訊
*/
private Position position;
/**
* 起始點(王子的起始位置),經由當前點的父節點後,到當前點的距離(僅包括垂直和水平直線上的)
*/
private int distanceOfPrince;
/**
* 當前點到目標點(公主位置)的距離
*/
private int distanceOfPrincess;
/**
* 父節點的權值
*/
private PositionWeight father;
/**
* 總開銷:包括起始點到當前點和當前點到終點的開銷之和
*/
private int cost;
public PositionWeight(Position position)
{
this.position = position;
}
public PositionWeight(Position position, PositionWeight father,
PositionWeight target)
{
this(position);
countDistanceToTarget(target);
updateByFather(father);
}
/**
* 獲取父子節點間的距離:對角線為14,水平、垂直為10
*/
public int getDistanceFromAttemptFather(PositionWeight father)
{
Position fatherPosition = father.getPosition();
return fatherPosition.getOffsetOfDistance(getPosition());
}
/**
* 更新父節點,並設定當前點的權值
*/
public void updateByFather(PositionWeight father)
{
setFather(father);
int distanceOfPrince = getDistanceFromAttemptFather(father);
setDistanceOfPrince(distanceOfPrince + father.getDistanceOfPrince());
setCost(getDistanceOfPrince() + getDistanceOfPrincess());
}
public Position getPosition()
{
return position;
}
public PositionWeight getFather()
{
return father;
}
public int getCost()
{
return cost;
}
public int getDistanceOfPrince()
{
return distanceOfPrince;
}
/**
* 獲取花費的總開銷
*/
public int getSpendTime()
{
return getCost() / STRAIGHT_DISTANCE;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result
+ ((position == null) ? 0 : position.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PositionWeight other = (PositionWeight) obj;
if (position == null)
{
if (other.position != null)
return false;
}
else
if (!position.equals(other.position))
return false;
return true;
}
@Override
public String toString()
{
return "PositionWeight [position=" + position + ", distanceOfPrince="
+ distanceOfPrince + ", distanceOfPrincess="
+ distanceOfPrincess + ", father=" + father.getPosition()
+ ", cost=" + cost + "]";
}
/**
* 設定到目標節點的距離
*/
private void countDistanceToTarget(PositionWeight target)
{
Position targetPosition = target.getPosition();
int distanceToTarget = getPosition()
.getDistanceOfTarget(targetPosition);
setDistanceOfPrincess(distanceToTarget);
}
private void setDistanceOfPrince(int distanceOfPrince)
{
this.distanceOfPrince = distanceOfPrince;
}
private int getDistanceOfPrincess()
{
return distanceOfPrincess;
}
private void setDistanceOfPrincess(int distanceOfPrincess)
{
this.distanceOfPrincess = distanceOfPrincess;
}
private void setFather(PositionWeight father)
{
this.father = father;
}
private void setCost(int cost)
{
this.cost = cost;
}
}
import java.util.ArrayList;
import java.util.List;
import javax.lang.model.element.UnknownElementException;
/**
* 迷宮地圖
*
* 類名稱:Maze 類描述: 建立人:dobuy
*
*/
public class MazeMap
{
/**
* 迷宮中的原點(0,0)
*/
private Position originPosition;
/**
* 迷宮中的最大邊界點
*/
private Position edgePosition;
/**
* 王子的位置
*/
private Position princePosition;
/**
* 公主的位置
*/
private Position princessPosition;
/**
* 所有可達的位置集合(非牆壁)
*/
private List<Position> allReachablePositions;
public MazeMap()
{
allReachablePositions = new ArrayList<Position>();
originPosition = new Position(0, 0);
}
public boolean isOverEdge(Position position)
{
if (getOriginPosition().getX() > position.getX()
|| getOriginPosition().getY() > position.getY()
|| getEdgePosition().getX() < position.getX()
|| getEdgePosition().getY() < position.getY())
{
return true;
}
return false;
}
/**
* 判斷是否是牆
*
*/
public boolean isWall(Position currentPosition)
{
if (isOverEdge(currentPosition) || isPrincess(currentPosition)
|| getPrincePosition().equals(currentPosition))
{
return false;
}
return !getAllReachablePositions().contains(currentPosition);
}
/**
* 判斷當前位置是否是公主位置
*
*/
public boolean isPrincess(Position currentPosition)
{
return getPrincessPosition().equals(currentPosition);
}
/**
* 初始化迷宮地址(座標轉換成點物件),並解析出王子的位置和公主的位置
*
* @param mazeMap 二維座標表示的迷宮地圖
*
*/
public void initMazeMap(char[][] mazeMap)
{
if (mazeMap == null || mazeMap.length <= 0)
{
throw new UnknownElementException(null, "null error");
}
for (int column = 0; column < mazeMap[0].length; column++)
{
for (int row = 0; row < mazeMap.length; row++)
{
parseMazePosition(new Position(row, column),
mazeMap[row][column]);
}
}
edgePosition = new Position(mazeMap.length, mazeMap[0].length);
}
public Position getPrincePosition()
{
return princePosition;
}
public Position getPrincessPosition()
{
return princessPosition;
}
/**
* 解析迷宮的位置資訊
*/
private void parseMazePosition(Position currentPosition, char thing)
{
switch (thing)
{
case '.':
getAllReachablePositions().add(currentPosition);
break;
case '*':
break;
case 'S':
setPrincePosition(currentPosition);
break;
case 'P':
setPrincessPosition(currentPosition);
break;
default:
throw new UnknownElementException(null, thing);
}
}
private Position getOriginPosition()
{
return originPosition;
}
private Position getEdgePosition()
{
return edgePosition;
}
private void setPrincePosition(Position princePosition)
{
this.princePosition = princePosition;
}
private void setPrincessPosition(Position princessPosition)
{
this.princessPosition = princessPosition;
}
private List<Position> getAllReachablePositions()
{
return allReachablePositions;
}
}
import javax.lang.model.element.UnknownElementException;
/**
* 迷宮類:包含王子和迷宮地圖兩個物件
*
*/
public class Maze
{
/**
* 王子
*/
private Prince prince;
/**
* 迷宮地圖
*/
private MazeMap map;
private boolean isInitOK = true;
public Maze(int time, char[][] map)
{
this.map = new MazeMap();
prince = new Prince(time);
initMap(map);
}
public void initMap(char[][] map)
{
try
{
getMap().initMazeMap(map);
getPrince().setMap(getMap());
}
catch (UnknownElementException e)
{
// TODO log
isInitOK = false;
}
}
/**
* 遊戲開始:返回結果:-1表示營救失敗;0表示營救成功
*
*/
public int start()
{
if (!isInitOK)
{
return -1;
}
return getPrince().startToSearch();
}
private MazeMap getMap()
{
return map;
}
private Prince getPrince()
{
return prince;
}
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 王子
*
* 類名稱:Prince 類描述: 建立人:dobuy
*
*/
public class Prince
{
/**
* 營救公主失敗
*/
private final static int FAIL = -1;
/**
* 營救公主成功
*/
private final static int SUCCESS = 0;
/**
* 剩餘的時間
*/
private int time;
/**
* 迷宮地圖
*/
private MazeMap map;
/**
* 正待嘗試的位置集合(開啟列表)
*/
private List<PositionWeight> attemptPositions;
/**
* 已經經過的位置集合(關閉列表)
*/
private List<PositionWeight> passedPositions;
/**
* 公主位置
*/
private PositionWeight princessPosition;
/**
* 王子位置
*/
private PositionWeight princePosition;
/**
* 王子移動一步的所有偏移量
*/
private List<Position> offsets = Arrays.asList(new Position[] {
new Position(1, 0), new Position(0, 1), new Position(-1, 0),
new Position(0, -1) });
public Prince(int time)
{
this.time = time;
this.attemptPositions = new ArrayList<PositionWeight>();
this.passedPositions = new ArrayList<PositionWeight>();
}
/**
* 開始尋找公主
*/
public int startToSearch()
{
reset();
if (getPrincePosition().getPosition() == null
|| getPrincessPosition().getPosition() == null || time < 0)
{
return FAIL;
}
// 1、新增王子自己的起始位置
getAttemptPositions().add(getPrincePosition());
// 2、通過移動維護待嘗試列表和已經嘗試的列表
attemptMove();
// 3、已經營救成功或者時間耗盡或者無法營救時,統計結果返回
return getSpendTime();
}
/**
* 設定迷宮地圖
*/
public void setMap(MazeMap map)
{
this.map = map;
}
/**
* 重置
*
*/
private void reset()
{
// 清空待嘗試的列表
getAttemptPositions().clear();
// 清空已經嘗試的列表
getPassedPositions().clear();
// 初始化王子的位置
Position princePosition = getMap().getPrincePosition();
setPrincePosition(new PositionWeight(princePosition));
// 初始化公主的位置
Position princessPosition = getMap().getPrincessPosition();
PositionWeight princessPositionWeight = new PositionWeight(
princessPosition);
setPrincessPosition(princessPositionWeight);
}
/**
* 可預知式移動
*
*/
private void attemptMove()
{
// 1、在如下2種情況下均結束:1)只要在待嘗試列表中發現了公主,表明已經找到; 2)迷宮中所有可達的點都遍歷完成,仍然無法找到
if (getAttemptPositions().contains(getPrincessPosition())
|| getAttemptPositions().isEmpty())
{
return;
}
// 2、獲取最新加入的開銷最小的節點
PositionWeight minPositionWeight = getMinPositionWeight();
// 3、從待嘗試列表中移除開銷最小節點
getAttemptPositions().remove(minPositionWeight);
// 4、把找到的開銷最小節點加至已經嘗試的列表
getPassedPositions().add(minPositionWeight);
// 5、對當前的開銷最小節點進行嘗試,找出其所有子節點
List<PositionWeight> subPositionWeights = getReachableSubPositions(minPositionWeight);
// 6、把所有子節點按照一定條件新增至待嘗試列表
for (PositionWeight subPositionWeight : subPositionWeights)
{
addPositionWeight(minPositionWeight, subPositionWeight);
}
// 7、重複以上操作
attemptMove();
}
/**
* 王子從當前移動一步,可達的位置(忽略牆)
*
*/
private List<PositionWeight> getReachableSubPositions(PositionWeight father)
{
List<PositionWeight> subPositionWeights = new ArrayList<PositionWeight>();
Position fatherPosition = father.getPosition();
PositionWeight subPositionWeight = null;
Position subPosition = null;
for (Position offset : offsets)
{
subPosition = fatherPosition.offset(offset);
subPositionWeight = new PositionWeight(subPosition, father,
getPrincessPosition());
// 子節點越界或者是牆壁或者已經在嘗試過的列表中時,不做任何處理
if (getMap().isOverEdge(subPosition)
|| getMap().isWall(subPosition)
|| isInPassedTable(subPositionWeight))
{
continue;
}
subPositionWeights.add(subPositionWeight);
}
return subPositionWeights;
}
/**
* 新增一個點
*
*/
private void addPositionWeight(PositionWeight father,
PositionWeight positionWeight)
{
// 在待嘗試列表中已經包含了當前點,則按照一定條件更新其父節點及其權值,否則直接新增
if (getAttemptPositions().contains(positionWeight))
{
updateCostByFather(father, positionWeight);
}
else
{
getAttemptPositions().add(positionWeight);
}
}
/**
* 計算花費的時間
*/
private int getSpendTime()
{
if (getAttemptPositions().contains(getPrincessPosition()))
{
int princessIndex = getAttemptPositions().indexOf(
getPrincessPosition());
PositionWeight princess = getAttemptPositions().get(princessIndex);
return princess.getSpendTime() <= time ? SUCCESS : FAIL;
}
return FAIL;
}
/**
* 從待嘗試列表中查詢總開銷值最小的點(如果有幾個相同開銷的最小點,取靠近隊尾的)
*
*/
private PositionWeight getMinPositionWeight()
{
PositionWeight minPositionWeight = getAttemptPositions().get(0);
for (PositionWeight positionWeight : getAttemptPositions())
{
if (minPositionWeight.getCost() >= positionWeight.getCost())
{
minPositionWeight = positionWeight;
}
}
return minPositionWeight;
}
/**
* 如果從父節點移動至子節點的G值小於子節點之前的G值(前提是子節點已經在開啟列表中),則更新子節點的父節點及G值
*/
private void updateCostByFather(PositionWeight father,
PositionWeight subPosition)
{
int distanceOfAttemptFather = subPosition
.getDistanceFromAttemptFather(father);
int distanceOfPrince = father.getDistanceOfPrince()
+ distanceOfAttemptFather;
if (distanceOfPrince < subPosition.getDistanceOfPrince())
{
subPosition.updateByFather(father);
}
}
private MazeMap getMap()
{
return map;
}
private boolean isInPassedTable(PositionWeight positionWeight)
{
return getPassedPositions().contains(positionWeight);
}
private List<PositionWeight> getAttemptPositions()
{
return attemptPositions;
}
private List<PositionWeight> getPassedPositions()
{
return passedPositions;
}
private PositionWeight getPrincessPosition()
{
return princessPosition;
}
private void setPrincessPosition(PositionWeight princessPosition)
{
this.princessPosition = princessPosition;
}
private PositionWeight getPrincePosition()
{
return princePosition;
}
private void setPrincePosition(PositionWeight princePosition)
{
this.princePosition = princePosition;
}
}
單元測試類:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/**
*
* 類名稱:MazeTest 類描述: 建立人:dobuy
*
*/
public class MazeTest
{
private Maze maze;
private char[][] map;
/**
* 營救公主失敗
*/
private final static int FAIL = -1;
/**
* 營救公主成功
*/
private final static int SUCCESS = 0;
/**
* testStart01 正常可達情況
*/
@Test
public void testStart01()
{
map = new char[][] { { '.', '.', '.', '.' }, { '.', '.', '.', '.' },
{ '.', '.', '.', '.' }, { 'S', '*', '*', 'P' } };
maze = new Maze(5, map);
assertEquals(maze.start(), SUCCESS);
}
/**
* testStart02 正常不可達情況
*/
@Test
public void testStart02()
{
map = new char[][] { { '.', '.', '.', '.' }, { '.', '.', '.', '.' },
{ '.', '.', '.', '.' }, { 'S', '*', '*', 'P' } };
maze = new Maze(2, map);
assertEquals(maze.start(), FAIL);
}
/**
* testStart03 引數異常
*/
@Test
public void testStart03()
{
map = null;
maze = new Maze(2, map);
assertEquals(maze.start(), FAIL);
map = new char[][] {};
maze = new Maze(2, map);
assertEquals(maze.start(), FAIL);
map = new char[][] { { '.', '.', '.', '.' }, { '.', '.', '.', '.' },
{ '.', '.', '.', '.' }, { '.', '.', '.', '.' } };
maze = new Maze(2, map);
assertEquals(maze.start(), FAIL);
map = new char[][] { { '.', '.', '.', '.' }, { '.', '.', '.', '.' },
{ 'S', '.', '.', 'P' }, { '.', '.', '.', '.' } };
maze = new Maze(-1, map);
assertEquals(maze.start(), FAIL);
}
/**
* testStart04 臨界值
*/
@Test
public void testStart04()
{
map = new char[][] { { '*', '*', '*', '*' }, { '*', '*', '*', '*' },
{ '*', '*', '*', '.' }, { 'S', '*', '*', 'P' } };
maze = new Maze(2, map);
assertEquals(maze.start(), FAIL);
map = new char[][] { { '.', '.', '.', '.' }, { '.', '.', '.', '.' },
{ 'S', 'P', '.', '*' }, { '.', '.', '.', '.' } };
maze = new Maze(1, map);
assertEquals(maze.start(), SUCCESS);
}
}