@
前些日子我有兄弟給我打電話,問我會不會人工智能,來實現一個機器人在倉庫自動尋路的功能。因為沒有接觸過這樣的場景,但是自己又比較對此興趣,所以就看了一些自動尋路的算法,比如:基於二叉樹的深度優先遍歷、D Star、A Star算法,其中我感覺A Star算法最好。下面我給大家介紹一下,首次實現的語言是Java,但是Java不太直觀,又不想使用Java的圖形界面,所以就使用JS+HTML來實現的,首先展示一下效果圖。
效果圖如下:
1、什么是A Start算法
A*搜索算法是求出在一個二維平面中從起點到終點最低運算代價的算法,它可以算出A點到B點的最短距離,也就是最優路徑。常見的應有主要是在游戲中,人物的自動尋路;機器人探路;交通路線導航等。
2、A Star算法的原理和流程
2.1 前提
在講述A Star算法之前,需要聲明下列這些屬性:
(1)從起點開始擴散的節點;
(2)最短距離計算公式:F = G + H;
(3)歐幾里得距離計算公式:p = $\sqrt (x_2 - x_1)^2+(y_2 - y_1)^2$(其實就是勾股定理);
(4)OPENLIST 和 CLOSELIST;
上面的屬性和公式不懂沒關系,下面我會對他們一一進行詳細介紹。非常簡單!
2.1.1 從起點開始擴散的節點
我們在HTML頁面上使用橫和 豎畫出來的格子。所謂擴散就是以 起點 為基點向上、下、左、右四個放向進行擴散,這些擴展的節點就是可以走的“路”。如下圖所示黃色的方格就是擴散的點:
PS:A Star有四個方向和八個方向的擴散。擴展四個方向的節點就是目前我們所說的;八個方向是還包含了,上左、上右、下左、下右四個方向的節點。我們通篇使用的是四個方向的擴展。
2.1.2 最短距離計算公式:F = G + H
如何在擴散的節點中找到最優也就是最短的一條路呢?就需要用到這個公式:F=G+H。那么這個公式里面的屬性都代表什么意識呢?下面我們就說明一下:
(1)G:
表示從起點到擴散的四個節點的距離,換句話說就是從起點到擴散的四個節點需要移動的格子。G的值可以使用歐幾里得距離計算公式進行計算。
如下圖:
(2)H:
表示從起點開始,到終點需要移動的格子(注意:忽略障礙物,可以從障礙物中穿過去),這個距離需要通過 歐幾里得距離計算公式 公式算出來。當然你沒必要一定要使用歐幾里得距離計算公式,你還可以單純的將起點到終點的x報坐標差值和y坐標差值進行相加即可。
如下圖:
(3)F:
F = G + H,在擴散節點中F的值最小的就是我們需要走的節點。也就是最短的路徑。
2.1.3 歐幾里得距離計算公式
這個公式是用來計算H和G的。公式:p = $\sqrt (x_2 - x_1)^2+(y_2 - y_1)^2$ 。其實就是終點的x坐標減去起點的x坐標的平方 + 終點的y坐標減去起點的y坐標的平方 開根號,這不就是勾股定理嘛。
2.1.4 OPENLIST 和 CLOSELIST
OPENLIST和CLOSELIST代表兩個“容器”(“容器”在代碼中就是兩個集合,使用List集合或者數組聲明都可以)。這個兩個“容器”存放的內容和作用如下:
(1)OPENLIST
用於存儲擴散的節點。剛開始由起點開始向四個方向擴散的節點就需要放到OPENLIST集合中(如果擴散的節點是障礙物或者是在CLOSELIST中已經存在則不放入)。OPENLIST是主要遍歷的集合,計算F值的節點都是來自這個集合。
(2)CLOSELIST
用於存儲起點、障礙物節點、走過的點。在擴散節點的時候,需要到CLOSELIST集合中去檢查,如果擴散的節點D已經在CLOSELIST集合中了(根據坐標進行判斷),或者是D節點是障礙物那么就跳過此節點。走過的點也需要放到CLOSELIST中去。
走過的點如下圖所示:
2.2 流程
2.2.1 第一步:擴散
從起點開始向上下左右擴散四個節點。假如起點的坐標為(x:3,y:2),那么四個節點為:上(x:2,y:2)、下(x:4,y:2)、左(x:3,y:1)、右(x:3,y:3)。
2.2.2 第二步:檢查節點
遍歷CLOSELIST集合,判斷擴散的這四個節點是否存在於CLOSELIST或者OPENLIST中,如果不存在放到OPENLIST中,反之跳過該節點。如果該節點是障礙物也需要跳過。
2.2.3 第三步:計算F的值
遍歷OPENLIST集合,並計算集合中節點的F的值,找出F值為最小的節點(距離最近)minNode,這個節點就是要走的節點。然后把除了mindNode之外的其它擴散的節點放入到CLOSELIST中。
2.2.4 第四步:改變起點的位置
通過第三步我們找到了F值最小的一個節點minNode,那么就把起點等於minNode。然后繼續進行擴散重復上面的四個步驟,直至在擴散的節點中包含終點我們走過的節點就是最短路徑。
3.A Star算法代碼實現
Java代碼我就不放出來了,如果想要的可以評論區留言。下面是用JS寫的,本人在其他文章里面也說過我的JS就是二半吊子(但是注釋寫的多)。寫的不好的地方大家指出,我會即時更正!
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>尋路02</title>
<style>
*{
margin: 0;
padding: 0;
}
#con{
width: 100%;
height: 100%;
}
body{
font-size: 0px;
}
#map{
width: 800px;
height: 800px;
/*border: 1px gray solid;*/
margin-top: 20px;
border-radius: 5px;
/*background-image: url("store/grass.png");*/
}
.square {
width: 40px;
height: 40px;
border: 1px gray solid;
/*background-image: url("store/tree01.png");*/
display: inline-block;
box-sizing:border-box;
color: red;
}
.roadblock{
width: 1px;
height: 1px;
border: 1px black solid;
background-color: black;
}
.p{
color: red;
margin: 0px;
padding: 0px;
display: inline-block;
}
</style>
</head>
<body>
<div id="con">
<button onclick="drawOther();">隨機生成障礙物</button>
<button onclick="startFindWay()">開始尋路</button>
<button onclick="stop()">停止</button>
<div id="map">
</div>
</div>
</body>
</html>
JS:
<script>
window.onload=function () {
init();
drawMAP();
}
//地圖div
var bg = document.querySelector('#map');
//開放集合
var openList=[];
//閉合集合
var closeList=[];
//起點
var startNode={};
//終點
var endNode = {};
//在由當前節點擴散的節點中 F值最小的一個節點
// count是一個計數器,用來判斷是否進入了死胡同
var minF = {topNode:'', F:'' ,G:'',H:'',x:'',y:''};
//當期節點
var currentNode = {};
//繪制地圖
function drawMAP() {
for(var i = 0 ; i < 20; i++){
for(var j = 0; j < 20;j ++ ){
var div = document.createElement('div');
div.className = 'square'
var p = document.createElement('p');
p.className = 'p';
p.innerHTML='('+i+','+j+')';
div.append(p)
div.id = i+'-'+j;
bg.append(div);
}
}
}
//初始化
function init() {
//添加起點和終點
startNode.x=1;
startNode.y=2;
startNode.des='start';
endNode.x =15;
endNode.y = 8;
endNode.des='end';
//添加障礙物
openList.push(startNode);
//將當前節點設置為startNode
currentNode = startNode;
}
//繪制障礙物、起點、終點
function drawOther() {
//繪制起點
var idStart = startNode.x+'-'+startNode.y;
document.getElementById(idStart).style.backgroundColor='red';
//繪制終點
var idEnd = endNode.x +'-'+endNode.y;
document.getElementById(idEnd).style.backgroundColor='blue';
randCreatBlock();
}
//隨機生成障礙物
function randCreatBlock() {
for (let i = 0; i < 100; i++) {
var x = Math.floor(Math.random()*(20));
var y = Math.floor(Math.random()*(20));
if ( x == startNode.x && y == startNode.y) {return ;}
if(x == endNode.x && y == endNode.y){return ;}
var point = x+'-'+y;
document.getElementById(point).style.backgroundColor = 'black';
var node = {x:x,y:y};
closeList.push(node);
}
}
//尋路
function findWay() {
//擴散上下左右四個節點
var up ={topNode:'', F:'',G:'',H:'',x:currentNode.x-1,y:currentNode.y};
var down ={topNode:'', F:'',G:'',H:'',x:currentNode.x+1,y:currentNode.y};
var left ={topNode:'', F:'',G:'',H:'',x:currentNode.x,y:currentNode.y-1};
var right ={topNode:'', F:'',G:'',H:'',x:currentNode.x,y:currentNode.y+1};
//檢查這些擴散的節點是否合法,如果合法放到openlist中
checkNode(up);
checkNode(down);
checkNode(left);
checkNode(right);
//移除已擴散完畢的節點移除,並放到closeList中去
removeNode();
//計算F
computersF();
}
function checkNode(node) {
//校驗擴散的點是否超過了地圖邊界
if(node.x<0||node.y<0){
return ;
}
//如果node存在closelist中則忽略
for (let i = 0; i < closeList.length; i++) {
if (closeList[i].x == node.x && closeList[i].y == node.y) {
return;
}
}
for (let i = 0; i <openList.length; i++) {
if(openList[i].x==node.x&&openList[i].y==node.y){
return;
}
}
if(node.topNode == '' ||node.topNode == null){
node.topNode = currentNode;
}
//如果擴散的這些節點 一個也沒有存到openList中,那么說明進入了死胡同
openList.push(node);
changeColor(node.x,node.y,'k');
}
//改變顏色
function changeColor(x,y,desc) {
var id = x+'-'+y;
if(desc == 'k'){
document.getElementById(id).style.backgroundColor = 'yellow';
}
if(desc == 'r'){
document.getElementById(id).style.backgroundColor = 'pink';
}
}
//計算FGH
function computersF() {
var x = endNode.x;
var y = endNode.y;
for (let i = 0; i < openList.length; i++) {
//計算H
var hx = parseInt(x) - parseInt(openList[i].x);
if(hx<0){
hx = -(parseInt(x) - parseInt(openList[i].x));
}
var hy = parseInt(y) - parseInt(openList[i].y);
if(hy<0){
hy = -(parseInt(y) - parseInt(openList[i].y));
}
var H = hx+hy;
openList[i].H= H;
//計算G
var G = Math.sqrt(Math.floor(Math.pow(parseInt(currentNode.x) - parseInt(openList[i].x),2))+
Math.floor(Math.pow(parseInt(currentNode.y) - parseInt(openList[i].y),2)));
openList[i].G= G;
//計算F
var F = G + H;
openList[i].F = F;
if(minF.F==''){
minF = openList[i];
}else {
if(minF.F>F){
minF = openList[i];
}
}
}
//201和204行代碼把openList賦值給了minF,openList並沒有定義count,count為undefined
// 所以需要判斷
if(minF.count==undefined){
minF.count = 0;
}
minF.count++;
console.log(this.minF.count);
//將當前節點設置為F最小的節點
currentNode = minF;
if(minF.count!=undefined&&minF.count>1){
//說明進入了死胡同
//1.將此節點放到closeList中
this.removeNode();
//2.在openList中去尋找 僅次於 此節點(進入死胡同)的其它節點
var minFSecond = openList[0];
for (let i = 0; i < openList.length; i++) {
console.log(openList[i])
if(minFSecond.F>=openList[i].F){
minFSecond = openList[i];
}
}
if(minFSecond.count==undefined){
minFSecond.count = 0;
}
minF = minFSecond;
currentNode = minFSecond;
console.log(currentNode);
}
//並將當前節點的顏色變為紅色
var id= currentNode.x +'-'+currentNode.y;
document.getElementById(id).style.backgroundColor='red';
}
//移除節點
function removeNode() {
var index = openList.indexOf(currentNode);
openList.splice(index,1);
closeList.push(currentNode);
//並將當前節點的顏色改變
changeColor(currentNode.x,currentNode.y,'r');
}
var myStart;
// 啟動
function startFindWay(){
myStart = setInterval(function startFindWay() {
findWay();
if(minF.x===endNode.x&&minF.y===endNode.y){
clearInterval(myStart);
return;
}
},100);
}
//停止
function stop(){
clearInterval(myStart);
}
</script>
4. 結語
A Star擴散四個方向的算法就這些。如果有時間可以把擴散八個方向的總結一下。寫這篇文章我想向大家提供的是A Star算法的過程與實現的思路,但是如果想要真正運用還需要考慮很多東西,要貼合自己的場景。上面的代碼還有問題。希望大家指出來,我會即時更正。還有什么不懂的可以在評論區討論。
路漫漫其修遠兮,吾將上下而求索