直接上代码仓库https://github.com/applelee/a-plus
先上一个结果图
关于A*算法的细节,网上有太多的讲解,我这里就不做搬运工了。
本着搞明白算法,不如理清思路的道理,我在这里就简单的把我的代码思路讲解下,并附上部分代码与截图。
第一步 生成地图
在写我们的核心代码前我们先要一个简单的地图,这里我使用的一个15 * 15方格子组成的场景,加上简单的样式后形成了下面的样子(这里我将每个格子的矢量显示了出来,方便理解,后面不再显示矢量)。
有了基本的地图结构,接下来就是添加障碍物,我的方法是在生成基本地图的同时加入一个随机数来判断是否生成障碍物,同时我们可以在生成障碍物的同时得到一个障碍物的矢量栈备用。
附上部分代码(这部分代码放在例子文件a+.html中)
const box = document.getElementById('container');
// 场景高宽
const box_w = box_h = 600;
// 格子尺寸
const el_w = el_h = 40;
// 列
const col = box_w / el_w;
// 行
const row = box_h / el_h;
// 障碍物
const obstacles = [];
// 开始位置矢量
let startVector = [];
// 终点位置矢量
let endVector = [];
// 点击格子的次数
let clickCount = 0;
// 生成格子
for (let i = 0; i < row; i ++) {
for (let j = 0; j < col; j ++) {
const element = document.createElement('div');
// 生成 0 - 7 随机数
const randomNum = Math.random() * 834819 & 7;
// const randomNum = (j/33, i/33) * 834819 & 7;
element.setAttribute('x', j);
element.setAttribute('y', i);
element.setAttribute('id', `${j}_${i}`);
element.setAttribute('style', `left: ${j * el_w}px;top: ${i * el_h}px`);
// 显示格子的矢量
element.textContent = `${j}, ${i}`;
// 生成障碍物
if (randomNum > 5) {
obstacles.push([j, i]);
element.style.background = '#666';
}
box.appendChild(element);
element.addEventListener('click', clickFn);
}
}
// 点击事件的方法
function clickFn () {
clickCount += 1;
// 获取两个格子的间的最短路径
if (clickCount === 1) {
startVector = [Number(this.getAttribute('x')), Number(this.getAttribute('y'))];
} else if (clickCount === 2) {
clickCount = 0;
endVector = [Number(this.getAttribute('x')), Number(this.getAttribute('y'))];
}
}
接下来需要通过点击方格子的事件,来获取我们的起点与终点来获取对应矢量。
这样渲染层的工作就准备好了。
第二部 逻辑
第一部分我们已经为我们的核心代码准备好了三个重要的参数。
1、场景的大小(15 * 15)
2、障碍物矢量栈
3、点击格子获得的起点与终点的矢量
options = {
// 起点矢量
startVector: [0, 0],
// 终点矢量
endVector: [1, 1],
// 场景大小 width height
screenSize: [15, 15],
// 障碍矢量集合
obstacles: [...],
}
算法的选择
关于A*的算法,我看网上大多都是采用的贪心算法。但是贪心算法有个弊端就是,很多时候无法获得全局最优解。特别是在障碍物分布复杂的时候。
上图是贪心算法得到的预期的最短路径。
上图同样是是贪心算法得到的非最短路径。
如果想要得到稳定的最优解就采用其它的算法策略。
比如穷举法
和贪心算法每次扩展探索只从所有有效分支路径中选中当前最有解进行下一次扩展探索(起点到当前点实际的长度 + 当前点离终点的预期长度最短)。
穷举法不会选取当前最优解的分支单独进行扩展,而是同时对所有的有效分支进行扩展探索。
上图1-20是贪心算法的结果。其它每一种颜色(除了深灰色是障碍物)都是是每次扩展探索派生出来的分支。
上图1-20是穷举法的结果。同样的其它每一种颜色(除了深灰色是障碍物)都是是每次扩展探索派生出来的分支。
这里可以看出来,穷举法在效率上是明显不如贪心算法的。
上面代码说到的扩展探索有几个个比较重要的变量
// 有效路径分支集合
// 主要用来储存每次探索生成的有效分支路径
// 如果在探索过程中某条分支无法产生新分支就视为无效分支,可以删除。
let branchs = new Map();
// 已探索过的矢量集合
// 每次探索成功后就把格子的位置信息储存下来
let solved = new Set();
我们在扩展探索的时候会对当前点的临近点进行探索,主要有两种。+探索(4向)*探索(8向)
// + 检测
const plus = [[0, -1], [1, 0], [0, 1], [-1, 0]];
// * 检测
const star = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]];
第三部分 完成条件
分为有解和无解两种情况
有解很好理解,无解的情况主要就一种。起点位置和终点位置被障碍物隔开,并且无法通过。
有解:当探索过程中,其中一条分支在探索临近点的时候发现与终点重合,就可以判断为完成,并将当前分支路径作为结果返回。
无解:当有效分支集合size为0的时候(地图所有的点被探索完毕,无法达到终点);循环或递归超过预设的阀值,通常地图尺度比较大的时候可能出现。
到这里整个逻辑代码结束,接下来就是返回路径数组交给html进行渲染
// 路径染色
function pathFillColor (path) {
if (path.length < 1) alert('此路不通,离终点十万八千里 -_-|||!');
const rgba = ramdomRGBA();
let count = 0;
const loop = (p = path) => {
const vector = p[count];
if (!vector) return;
const el = document.getElementById(`${vector[0]}_${vector[1]}`);
// el.textContent = count + 1;
el.style.background = rgba;
setTimeout(() => loop(p), 100)
document.getElementById('text').innerText = count;
count += 1;
if (count >= p.length) {
if (vector[0] !== endVector[0] || vector[1] !== endVector[1]) {
setTimeout(() => alert('此路不通,要么翻山,要么打洞 -_-|||!'));
}
return;
}
}
loop();
}
到这里基本讲解完了,以上的代码都是片段,完整的代码去仓库里下载例子吧。觉得不错记得给个star哦!!
仓库地址https://github.com/applelee/a-plus