1. 程式人生 > >營救公主(Java實現A*演算法解決迷宮問題)

營救公主(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);
	}
}