算法背景
A*(A-Star)算法是一种在图形平面上,有多个节点的路径中,求出最低通过成本的算法。其历史可以追溯到早期的图搜索算法,如Dijkstra算法和贪心最佳优先搜索(Greedy Best-First Search)。是人工智能、游戏开发、机器人路径规划等领域中最经典、最有效的寻路算法之一。
算法解释
A*算法的核心思想是结合了Dijkstra算法和Best-First-Search(BFS)算法的特点。他给每个格子赋予了一个权重 f(n) = g(n) + h(n) ,通过权重来决定遍历的顺序。
详细解释
首先得理解广度优先搜索(BFS),在方格中,BFS通过遍历起始格子上下左右的四个格子,将其压入一个队列 ,通过循环 出队-搜索-入队 来寻找最短路。
而A*算法则是在BFS的基础上使用了贪心策略,给每个格子赋予一个权重f(n),其中f(n) = g(n) + h(n) 。
-
g(n)为起点到当前点的最短距离。
-
h(n)为当前点到终点的估计距离。
A*算法则使用了优先队列 ,通过寻找路径代价最小的节点来寻找目标点,它保证了如果h (n )的估计值是准确的,那么找到的路径也是最短的。同时,A*算法比BFS减少了遍历点的数量,加快了路线寻找的速度。
【很好理解,当你要从一个地方到另一个地方,并且你已经知道终点的方位,及时没有导航,你很自然会优先朝着终点方向前进,即使和道路方向并不相同】
下图是每个格子的 g(n) 也就是当前距离起点的步数,这个很好理解。

下图是每个格子的 h(n) ,也就是每个格子距离终点的估计距离 ,下图使用了曼哈顿距离。

你会发现,在上图没有障碍的时候,每个格子的权重是一样的,无法体现路线的优化。如果使用欧几里得距离 ,就能体现出权重的作用,如下图:

距离的估计
有很多计算方式,比如:
1.曼哈顿距离:
- 定义:只能沿着网格的横纵方向移动,不能斜向移动。
-
适用场景:城市街区网格、只能走直线(上下左右)的环境,比如A*算法中常用的启发函数
-
特点:简单、计算快,但可能高估实际距离。

2.欧几里得距离:
- 定义:两点之间的"直线距离",也就是我们日常生活中说的"最短距离"。
-
适用场景:连续空间、物理世界中的距离计算,比如机器人导航、图像识别。
-
特点:真实距离,但计算稍慢,且可能包含浮点运算。

3. 切比雪夫距离:
- 定义:允许八方向移动(上下左右+对角线),取各坐标差值的最大值。
-
适用场景:可以斜向移动的游戏地图(如象棋中的国王移动),或某些特殊路径规划。
-
特点:比曼哈顿更灵活,但可能低估实际步数。

4. 八方向距离:
- 定义:允许八方向移动,但斜向移动的代价更高(比如√2倍),更接近真实情况。
近似写法:
-
适用场景:允许斜向移动但代价更高的地图,比如某些策略游戏或真实地形路径规划。
-
特点:比切比雪夫更精确,适合八方向移动的A*启发函数。

最短路证明
所以,A*算法得到的路径一定是最短路吗?答案是否定的。
我们已知BFS得到的路径一定是最短路,但是BFS的时间效率过低,我们添加了贪心的策略,权衡速度与最优性。
但是,如果我们处理好启发式函数的大小,可以保证A*路径的最优性,即启发项小于等于最优的那条路径。
在之前的方格示例中,A*得到的一定是最优路径,因为每个点的估计价值是通过当前点与终点的相对位置计算得出的,其有绝对性,即不同点的估计价值在同个距离算法下是绝对的。
但是在实际应用中,直接使用相对坐标来计算估计价值是不合理的,比如:

其中蓝色的为河流,我们预估通过河流所需时间需要10个单位,假设通过一个格子所需时间1个单位。
从起点开始,到河流的权重f(n)为16,而走上面路线权重为14,则走上面路线到达终点。但是事实上河流上有桥,可以直接通过,而河流路径明显比上面更短,丧失了最优性。
发现当h<=4的时候,其最短路径都是正确的,这就是启发项大小要小于等于最优路径。
在实际情况中,要合理设置初始权值,才能避免某些不必要的麻烦。
当然,比如你想要某个敌人不能走某条路,直接将初始权值设为无限大,就可以实现该功能。
算法示例
写了一个html文件,可以直观展现A*算法的过程与结果,可以复制到本地尝试一下:

代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A* 算法可视化演示(优化版)</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.description {
max-width: 700px;
text-align: center;
margin-bottom: 20px;
color: #555;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
button {
padding: 10px 15px;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.grid-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(20, 25px);
grid-template-rows: repeat(20, 25px);
gap: 1px;
border: 1px solid #ccc;
background-color: #ddd;
position: relative;
}
.cell {
width: 25px;
height: 25px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
transition: background-color 0.2s;
position: relative;
}
.cell:hover {
opacity: 0.8;
}
.wall { background-color: #333; }
.start { background-color: #2ecc71; }
.end { background-color: #e74c3c; }
.visited { background-color: #3498db; }
.path { background-color: #f1c40f; }
.current { background-color: #9b59b6; }
.frontier { background-color: #95a5a6; opacity: 0.6; }
.legend {
display: flex;
gap: 15px;
margin-top: 10px;
font-size: 14px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.legend-color {
width: 15px;
height: 15px;
border: 1px solid #ccc;
}
.info {
margin-top: 20px;
max-width: 600px;
background-color: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.info h3 {
margin-top: 0;
color: #2c3e50;
}
.info p {
margin: 5px 0;
font-size: 14px;
}
.mode-selector {
margin-bottom: 10px;
display: flex;
gap: 10px;
align-items: center;
}
select, input {
padding: 5px;
font-size: 16px;
}
.speed-control {
display: flex;
align-items: center;
gap: 10px;
}
.stats {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.stat-item {
font-size: 14px;
}
.stat-value {
font-weight: bold;
color: #3498db;
}
.heuristic-selector {
display: flex;
align-items: center;
gap: 10px;
}
</style>
</head>
<body>
<h1>A* 寻路算法可视化(优化版)</h1>
<div class="description">
点击网格设置起点(绿色)、终点(红色)和障碍物(黑色)。支持拖拽绘制障碍物,可切换不同启发式函数。
</div>
<div class="mode-selector">
<label for="mode">编辑模式: </label>
<select id="mode">
<option value="start">设置起点</option>
<option value="end">设置终点</option>
<option value="wall">绘制障碍墙</option>
<option value="erase">擦除障碍</option>
</select>
<div class="heuristic-selector">
<label for="heuristic">启发式函数: </label>
<select id="heuristic">
<option value="manhattan">曼哈顿距离</option>
<option value="euclidean">欧几里得距离</option>
<option value="chebyshev">切比雪夫距离</option>
<option value="octile">八方向距离</option>
</select>
</div>
</div>
<div class="controls">
<button id="startBtn">开始寻路</button>
<button id="stepBtn" disabled>单步执行</button>
<button id="autoBtn" disabled>自动播放</button>
<button id="resetBtn">重置网格</button>
<button id="clearPathBtn">清除路径</button>
<button id="mazeBtn">生成迷宫</button>
<div class="speed-control">
<label for="speed">速度: </label>
<input type="range" id="speed" min="10" max="500" value="100" step="10">
<span id="speedValue">100ms</span>
</div>
</div>
<div class="grid-container">
<div class="grid" id="grid"></div>
<div class="legend">
<div class="legend-item">
<div class="legend-color start"></div>
<span>起点</span>
</div>
<div class="legend-item">
<div class="legend-color end"></div>
<span>终点</span>
</div>
<div class="legend-item">
<div class="legend-color wall"></div>
<span>障碍</span>
</div>
<div class="legend-item">
<div class="legend-color frontier"></div>
<span>待探索</span>
</div>
<div class="legend-item">
<div class="legend-color visited"></div>
<span>已访问</span>
</div>
<div class="legend-item">
<div class="legend-color current"></div>
<span>当前节点</span>
</div>
<div class="legend-item">
<div class="legend-color path"></div>
<span>最终路径</span>
</div>
</div>
</div>
<div class="info">
<h3>算法信息</h3>
<p id="currentInfo">设置起点和终点,然后点击"开始寻路"。</p>
<div class="stats">
<div class="stat-item">路径长度: <span class="stat-value" id="pathLength">-</span></div>
<div class="stat-item">访问节点数: <span class="stat-value" id="visitedCount">-</span></div>
<div class="stat-item">开放列表大小: <span class="stat-value" id="openListSize">-</span></div>
<div class="stat-item">执行时间: <span class="stat-value" id="executionTime">-</span></div>
</div>
</div>
<script>
// 网格配置
const ROWS = 20;
const COLS = 20;
let grid = [];
let startCell = null;
let endCell = null;
let isRunning = false;
let isAutoRunning = false;
let openList = [];
let closedSet = new Set(); // 使用Set提高查找效率
let path = [];
let currentAlgorithmState = null;
let autoInterval = null;
let isMouseDown = false;
let startTime = 0;
let animationSpeed = 100;
// 优先队列实现(最小堆)
class PriorityQueue {
constructor() {
this.heap = [];
}
push(element) {
this.heap.push(element);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
const min = this.heap[0];
const end = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = end;
this.bubbleDown(0);
}
return min;
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[index].f < this.heap[parentIndex].f) {
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
index = parentIndex;
} else {
break;
}
}
}
bubbleDown(index) {
while (true) {
let minIndex = index;
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
if (leftChild < this.heap.length && this.heap[leftChild].f < this.heap[minIndex].f) {
minIndex = leftChild;
}
if (rightChild < this.heap.length && this.heap[rightChild].f < this.heap[minIndex].f) {
minIndex = rightChild;
}
if (minIndex !== index) {
[this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
index = minIndex;
} else {
break;
}
}
}
get length() {
return this.heap.length;
}
contains(element) {
return this.heap.includes(element);
}
update(element) {
const index = this.heap.indexOf(element);
if (index !== -1) {
this.bubbleUp(index);
this.bubbleDown(index);
}
}
}
// 初始化网格
function initGrid() {
const gridElement = document.getElementById('grid');
gridElement.innerHTML = '';
grid = [];
for (let row = 0; row < ROWS; row++) {
const gridRow = [];
for (let col = 0; col < COLS; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = row;
cell.dataset.col = col;
cell.addEventListener('mousedown', () => {
isMouseDown = true;
handleCellClick(row, col);
});
cell.addEventListener('mouseenter', () => {
if (isMouseDown) {
handleCellDrag(row, col);
}
});
cell.addEventListener('mouseup', () => {
isMouseDown = false;
});
gridElement.appendChild(cell);
gridRow.push({
element: cell,
row,
col,
isWall: false,
f: Infinity,
g: Infinity,
h: 0,
parent: null,
inOpenList: false
});
}
grid.push(gridRow);
}
// 全局鼠标释放事件
document.addEventListener('mouseup', () => {
isMouseDown = false;
});
resetAlgorithmState();
}
function resetAlgorithmState() {
openList = new PriorityQueue();
closedSet = new Set();
path = [];
isRunning = false;
isAutoRunning = false;
currentAlgorithmState = null;
startTime = 0;
document.getElementById('startBtn').disabled = false;
document.getElementById('stepBtn').disabled = true;
document.getElementById('autoBtn').disabled = true;
clearInterval(autoInterval);
updateInfoText('设置起点和终点,然后点击"开始寻路"。');
updateStats();
// 清除之前的访问和路径标记,但保留墙、起点和终点
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = grid[row][col];
cell.element.classList.remove('visited', 'path', 'current', 'frontier');
cell.f = Infinity;
cell.g = Infinity;
cell.h = 0;
cell.parent = null;
cell.inOpenList = false;
}
}
}
function handleCellClick(row, col) {
if (isRunning) return;
const cell = grid[row][col];
const mode = document.getElementById('mode').value;
switch (mode) {
case 'start':
if (startCell) {
startCell.element.classList.remove('start');
}
cell.element.classList.remove('wall', 'end');
cell.element.classList.add('start');
cell.isWall = false;
startCell = cell;
break;
case 'end':
if (endCell) {
endCell.element.classList.remove('end');
}
cell.element.classList.remove('wall', 'start');
cell.element.classList.add('end');
cell.isWall = false;
endCell = cell;
break;
case 'wall':
if (cell !== startCell && cell !== endCell) {
cell.isWall = true;
cell.element.classList.add('wall');
}
break;
case 'erase':
if (cell !== startCell && cell !== endCell) {
cell.isWall = false;
cell.element.classList.remove('wall');
}
break;
}
}
function handleCellDrag(row, col) {
const cell = grid[row][col];
const mode = document.getElementById('mode').value;
if (mode === 'wall' && cell !== startCell && cell !== endCell) {
cell.isWall = true;
cell.element.classList.add('wall');
} else if (mode === 'erase' && cell !== startCell && cell !== endCell) {
cell.isWall = false;
cell.element.classList.remove('wall');
}
}
// 启发式函数
function heuristic(a, b) {
const type = document.getElementById('heuristic').value;
const dx = Math.abs(a.row - b.row);
const dy = Math.abs(a.col - b.col);
switch (type) {
case 'manhattan':
return dx + dy;
case 'euclidean':
return Math.sqrt(dx * dx + dy * dy);
case 'chebyshev':
return Math.max(dx, dy);
case 'octile':
return Math.max(dx, dy) + (Math.sqrt(2) - 1) * Math.min(dx, dy);
default:
return dx + dy;
}
}
function getNeighbors(cell) {
const neighbors = [];
const { row, col } = cell;
// 八个方向(包括对角线)
const directions = [
[-1, 0, 1], [0, 1, 1], [1, 0, 1], [0, -1, 1], // 上右下左
[-1, -1, 1.414], [-1, 1, 1.414], [1, 1, 1.414], [1, -1, 1.414] // 对角线
];
const allowDiagonal = document.getElementById('heuristic').value !== 'manhattan';
const limit = allowDiagonal ? 8 : 4;
for (let i = 0; i < limit; i++) {
const [dr, dc, cost] = directions[i];
const newRow = row + dr;
const newCol = col + dc;
if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
const neighbor = grid[newRow][newCol];
if (!neighbor.isWall) {
// 对角线移动时检查两边是否有墙
if (i >= 4) {
const side1 = grid[row + directions[i-4][0]][col + directions[i-4][1]];
const side2 = grid[row + directions[(i-3)%4][0]][col + directions[(i-3)%4][1]];
if (side1.isWall || side2.isWall) continue;
}
neighbors.push({cell: neighbor, cost});
}
}
}
return neighbors;
}
function startAlgorithm() {
if (!startCell || !endCell) {
alert('请先设置起点和终点!');
return;
}
resetAlgorithmState();
isRunning = true;
startTime = performance.now();
// 初始化开放列表,加入起点
openList = new PriorityQueue();
startCell.g = 0;
startCell.h = heuristic(startCell, endCell);
startCell.f = startCell.h;
startCell.inOpenList = true;
openList.push(startCell);
document.getElementById('startBtn').disabled = true;
document.getElementById('stepBtn').disabled = false;
document.getElementById('autoBtn').disabled = false;
currentAlgorithmState = {
found: false,
noPath: false
};
updateStats();
updateInfoText('A* 算法已启动,准备开始搜索...');
}
function stepAlgorithm() {
if (!isRunning || currentAlgorithmState.found || currentAlgorithmState.noPath) return;
if (openList.length === 0) {
currentAlgorithmState.noPath = true;
updateInfoText('搜索完成:没有找到路径。');
finishAlgorithm();
return;
}
// 从优先队列中取出f值最小的节点
const current = openList.pop();
current.inOpenList = false;
// 标记当前节点
if (current !== startCell && current !== endCell) {
current.element.classList.add('current');
current.element.classList.remove('frontier');
if (current.parent && current.parent !== startCell) {
current.parent.element.classList.remove('current');
}
}
// 如果找到终点
if (current === endCell) {
currentAlgorithmState.found = true;
reconstructPath(current);
const executionTime = (performance.now() - startTime).toFixed(2);
updateInfoText(`搜索完成:已找到最短路径!用时 ${executionTime}ms`);
finishAlgorithm();
return;
}
// 将当前节点移到关闭列表
closedSet.add(current);
if (current !== startCell && current !== endCell) {
current.element.classList.remove('current');
current.element.classList.add('visited');
}
updateInfoText(`正在检查节点 (${current.row}, ${current.col}) - f=${current.f.toFixed(2)}`);
// 检查所有邻居
const neighbors = getNeighbors(current);
for (const {cell: neighbor, cost} of neighbors) {
if (closedSet.has(neighbor)) continue;
const tentativeG = current.g + cost;
if (tentativeG < neighbor.g) {
neighbor.parent = current;
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor, endCell);
neighbor.f = neighbor.g + neighbor.h;
if (!neighbor.inOpenList) {
openList.push(neighbor);
neighbor.inOpenList = true;
if (neighbor !== endCell) {
neighbor.element.classList.add('frontier');
}
} else {
openList.update(neighbor);
}
}
}
updateStats();
}
function reconstructPath(current) {
path = [];
let temp = current;
while (temp) {
path.push(temp);
temp = temp.parent;
}
path.reverse();
// 动画显示路径
path.forEach((cell, index) => {
if (cell !== startCell && cell !== endCell) {
setTimeout(() => {
cell.element.classList.add('path');
cell.element.classList.remove('visited');
}, index * 20);
}
});
}
function finishAlgorithm() {
isRunning = false;
isAutoRunning = false;
document.getElementById('stepBtn').disabled = true;
document.getElementById('autoBtn').disabled = true;
clearInterval(autoInterval);
// 移除所有当前标记
document.querySelectorAll('.current').forEach(el => el.classList.remove('current'));
document.querySelectorAll('.frontier').forEach(el => el.classList.remove('frontier'));
const executionTime = (performance.now() - startTime).toFixed(2);
document.getElementById('executionTime').textContent = `${executionTime}ms`;
document.getElementById('pathLength').textContent = path.length > 0 ? path.length - 1 : '-';
}
function startAutoRun() {
if (isAutoRunning) {
stopAutoRun();
return;
}
isAutoRunning = true;
document.getElementById('autoBtn').textContent = '停止自动播放';
autoInterval = setInterval(() => {
if (currentAlgorithmState.found || currentAlgorithmState.noPath) {
stopAutoRun();
return;
}
stepAlgorithm();
}, animationSpeed);
}
function stopAutoRun() {
isAutoRunning = false;
clearInterval(autoInterval);
document.getElementById('autoBtn').textContent = '自动播放';
}
function updateInfoText(text) {
document.getElementById('currentInfo').textContent = text;
}
function updateStats() {
document.getElementById('visitedCount').textContent = closedSet.size;
document.getElementById('openListSize').textContent = openList.length;
document.getElementById('pathLength').textContent = path.length > 0 ? path.length - 1 : '-';
}
// 生成随机迷宫
function generateMaze() {
if (isRunning) return;
// 清除所有墙
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = grid[row][col];
if (cell !== startCell && cell !== endCell) {
cell.isWall = false;
cell.element.classList.remove('wall');
}
}
}
// 随机生成障碍物(30%概率)
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = grid[row][col];
if (cell !== startCell && cell !== endCell && Math.random() < 0.3) {
cell.isWall = true;
cell.element.classList.add('wall');
}
}
}
resetAlgorithmState();
}
// 速度控制
document.getElementById('speed').addEventListener('input', (e) => {
animationSpeed = parseInt(e.target.value);
document.getElementById('speedValue').textContent = `${animationSpeed}ms`;
if (isAutoRunning) {
stopAutoRun();
startAutoRun();
}
});
// 事件监听器
document.getElementById('startBtn').addEventListener('click', startAlgorithm);
document.getElementById('stepBtn').addEventListener('click', stepAlgorithm);
document.getElementById('autoBtn').addEventListener('click', startAutoRun);
document.getElementById('resetBtn').addEventListener('click', initGrid);
document.getElementById('clearPathBtn').addEventListener('click', resetAlgorithmState);
document.getElementById('mazeBtn').addEventListener('click', generateMaze);
// 初始化
initGrid();
// 设置默认起点和终点
handleCellClick(5, 2);
document.getElementById('mode').value = 'end';
handleCellClick(15, 17);
document.getElementById('mode').value = 'wall';
</script>
</body>
</html>