A*算法(寻路)
一.什么是A*算法
A*搜寻算法俗称A星算法。A*算法是比较流行的启发式搜索算法之一,被广泛应用于路径优化领域。它的独特之处是检查最短路径中每个可能的节点时引入了全局信息,对当前节点距终点的距离做出估计,并作为评价该节点处于最短路线上的可能性的量度。
A*改变它自己行为的能力基于启发式代价函数,启发式函数在游戏中非常有用。在速度和精确度之间取得折衷将会让你的游戏运行得更快。在很多游戏中,你并不真正需要得到最好的路径,仅需要近似的就足够了。而你需要什么则取决于游戏中发生着什么,或者运行游戏的机器有多快。 假设你的游戏有两种地形,平原和山地,在平原中的移动代价是1而在山地的是3,那么A星算法就会认为在平地上可以进行三倍于山地的距离进行等价搜寻。 这是因为有可能有一条沿着平原到山地的路径。把两个邻接点之间的评估距离设为1.5可以加速A*的搜索过程。然后A*会将3和1.5比较,这并不比把3和1比较差。然而,在山地上行动有时可能会优于绕过山脚下进行行动。所以花费更多时间寻找一个绕过山的算法并不经常是可靠的。 同样的,想要达成这样的目标,你可以通过减少在山脚下的搜索行为来打到提高A星算法的运行速率。若想如此可以将A星算法的山地行动耗费从3调整为2即可。这两种方法都会给出可靠地行动策略。
可采纳性
在一些问题的求解过程中,如果存在最短路径,无论在什么情况之下,如果一个搜索算法都能够保证找到这条最短路径,则称这样的搜索算法就具有可采纳性。 [2]
单调性
A*算法扩展的所有节点的序列的f值是递增的,那么它最先生成的路径一定就是最短的。
信息性
如何判断两种策略哪一个更好呢?具有的启发信息越多,搜索的状态就越少,越容易找到最短路径,这样的策略就更好。在两个启发策略中,如果有h(n1)
h(n2),则表示策略h:比h,具有更多的启发信息,同时h(n)越大表示它所搜索的空间数就越少。但是同时需要注意的是:信息越多就需要更多的计算时间,从而有可能抵消因为信息多而减少的搜索空间所带来的好处。
缺陷
A*算法进行下一步将要走的节点的搜索的时候,每次都是选择F值最小的节点,因此找到的是最优路径。但是正因为如此A*算法每次都要扩展当前节点的全部后继节点,运用启发函数计算它们的F值,然后选择F值最小的节点作为下一步走的节点。在这个过程中,OPEN表需要保存大量的节点信息,不仅存储量大是一个问题,而且在查找F值最小的节点时,需要查询的节点也非常多,当然就非常耗时,这个问题就非常严重了。再加上如果游戏地图庞大,路径比较复杂,路径搜索过程则可能要计算成千上万的节点,计算量非常巨大。因此,搜索一条路径需要一定的时间,这就意味着游戏运行速度降低。
二.实践
index.html
<!DOCTYPE html>
<html>
<head>
<title>A* Pathfinding Demo</title>
<style>
#grid {
display: grid;
grid-template-columns: repeat(10, 50px);
justify-content: center;
margin-top: 50px;
}
.cell {
border: 1px solid gray;
height: 50px;
width: 50px;
background-color: white;
}
.cell.start {
background-color: green;
}
.cell.end {
background-color: red;
}
.cell.wall {
background-color: black;
}
.cell.path {
background-color: yellow;
}
</style>
</head>
<body>
<div id="grid"></div>
<button type="button" id="clearCell">清空点位</button>
<button type="button" id="seekingWay">执行</button>
<script>
const CELL_COLORS = {
DEFAULT: "#fff",
WALL: "#000",
START: "#00f",
END: "#f00",
OPEN: "#0f0",
CLOSED: "#f0f",
PATH: "#ff0",
};
</script>
<script src="./Cell.js"></script>
<script src="./Grid.js"></script>
<script src="./AStar.js"></script>
<script src="./index.js"></script>
</body>
</html>
Cell.js
/**
* @description: 创建Cell
* @method: Cell
* @author: sgjy
* @date: 2024/3/25
* @lastEditors: sgjy
*/
class Cell {
constructor(row, col) {
this.row = row;
this.col = col;
this.isWall = false;
this.g = 0;
this.h = 0;
this.f = 0;
this.previous = null;
this.hasStartCell = false;
this.hasEndCell = false;
this.element = this.createElement();
// this.element.addEventListener("click", this.toggleWall.bind(this));
}
createElement() {
const element = document.createElement("div");
element.className = "cell";
return element;
}
// toggleWall() {
// this.isWall = !this.isWall;
// this.element.style.backgroundColor = this.isWall ? CELL_COLORS.WALL : CELL_COLORS.DEFAULT;
// }
getNeighbors() {
const neighbors = [];
const offsets = [{
row: -1,
col: 0
}, // 上方格子
{
row: 1,
col: 0
}, // 下方格子
{
row: 0,
col: -1
}, // 左侧格子
{
row: 0,
col: 1
}, // 右侧格子
];
for (const offset of offsets) {
const neighborRow = this.row + offset.row;
const neighborCol = this.col + offset.col;
if (neighborRow >= 0 && neighborRow < grid.rows && neighborCol >= 0 && neighborCol < grid.cols) {
neighbors.push(grid.cells[neighborRow][neighborCol]);
}
}
return neighbors;
}
}
Grid.js
/**
* @description: 创建Grid
* @method: Grid
* @author: sgjy
* @date: 2024/3/25
* @lastEditors: sgjy
*/
class Grid {
constructor(rows, cols, containerId) {
this.rows = rows;
this.cols = cols;
this.container = document.getElementById(containerId);
this.cells = [];
this.startCell = null;
this.endCell = null;
}
createCells() {
for (let row = 0; row < this.rows; row++) {
const rowCells = [];
for (let col = 0; col < this.cols; col++) {
const cell = new Cell(row, col);
cell.element.addEventListener('click', () => {
this.addStartAndEndCell(cell);
});
rowCells.push(cell);
this.container.appendChild(cell.element);
}
this.cells.push(rowCells);
}
}
clear() {
for (const rowCells of this.cells) {
for (const cell of rowCells) {
if (cell) {
cell.isWall = false;
cell.g = 0;
cell.h = 0;
cell.f = 0;
cell.previous = null;
cell.element.style.backgroundColor = CELL_COLORS.DEFAULT;
}
}
}
this.startCell = null;
this.endCell = null;
}
setStartCell(cell) {
this.startCell = cell;
cell.element.style.backgroundColor = CELL_COLORS.START;
}
setEndCell(cell) {
this.endCell = cell;
cell.element.style.backgroundColor = CELL_COLORS.END;
}
getStartCell() {
return this.startCell;
}
getEndCell() {
return this.endCell;
}
addStartAndEndCell(cell) {
console.log(cell);
if(!this.startCell) {
this.setStartCell(cell);
} else if (!this.endCell && cell !== this.startCell) {
this.setEndCell(cell);
}
}
}
index.js
const grid = createGrid();
let startCell = null; // 起点
let endCell = null; // 终点
// 点击格子事件处理函数
/* function handleCellClick(row, col, grid) {
const cell = grid[row][col];
if (!startCell) {
// 如果起点为空,则设置为点击的格子,并更新样式为红色
startCell = cell;
startCell.element.style.backgroundColor = CELL_COLORS.START;
grid.setStartCell(startCell);
} else if (!endCell && cell !== startCell) {
// 如果终点为空且点击的格子不是起点,则设置为终点,并更新样式为红色
endCell = cell;
endCell.element.style.backgroundColor = CELL_COLORS.END;
grid.setEndCell(endCell);
// 创建 A* 实例并执行搜索
const astar = new AStar(grid);
astar.search(startCell, endCell);
}
} */
// 创建网格
function createGrid(rows = 10, cols = 10, containerId = 'grid') {
const grid = new Grid(rows, cols, containerId);
grid.createCells();
return grid;
}
// 获取按钮元素
var button = document.getElementById('clearCell');
// 为按钮添加click事件监听
button.addEventListener('click', function() {
console.log('111');
grid.clear();
});
// 执行寻路
// 获取按钮元素
var button = document.getElementById('seekingWay');
// 为按钮添加click事件监听
button.addEventListener('click', function() {
// 设置起点
startCell = grid.getStartCell();
// 设置终点
endCell = grid.getEndCell();
// 创建 A* 实例并执行搜索
const astar = new AStar(grid);
astar.search(startCell, endCell);
});