網上有關A*算法的文章已經非常豐富了,各種語言各種思路都有,本來我實在不想再寫一篇,,但因爲最近工作動盪因此專門抽空又看了一下,然後就想寫個文章防止以後印象模糊,好歹看自己寫的東西可以很快回憶起來。如果是初次接觸A*算法的朋友可以先看看這篇參考文章,我這邊只是做一個總結,然後先貼上我之前的筆記吧:
二維數組map數據:
0=可通過的格子,S=起點,E=終點,B=障礙物
基礎定義:G=目標點到起點的步長總和 H=目標點到終點的步長總和(曼哈頓距離),F=兩總和之和
定義:OpenList,CloseList,current,adjacentCube,g(步長,這個例子假定爲1)
1.將起點A加入到OpenList中,遍歷OpenList,每次取最小F值的item,將其賦值給current,隨後將這個item從OpenList中刪除,加入到CloseList(例子中在這裏遍歷CloseList中是否存在目標點,有的話則立刻break,我個人覺得沒必要每次都遍歷,直接判斷取到的current是否目標路徑即可)。
2.獲取到該item四周的item(需要判斷map該位置是否爲"0"或"E",否則過濾,因爲障礙物和邊緣不能加入尋路OpenList中),設置X和Y並insert入adjacentCube中。
3.遍歷adjacentCube,檢查每個item是否已存在於OpenList中,兩種情況:
(1.)已存在OpenList中,則判斷如果current的G值+g步長和大於這個item的G值的話,則將這個item的parent置爲current,同時將這個item的G值更新爲current父物體的G值+g步長(主要是找一個相對最短的路徑,因爲最短路徑最終需要從終點通過parent回溯,而不是正向尋找一遍就結束了)。
(2.)未存在OpenList中,則設置這個item的各種屬性值(G,H,F)以及parent,然後將其insert入OpenList中。
4.重複以上步驟,直到找到目標點則break隨後開始回溯輸出。回溯的過程比較簡單,因爲之前有記錄parent,所以當前current的parent已經記錄了一條最短的路徑。
然後如果你看過原文章的代碼的話可能會發現那個變量g會有問題,源碼每次OpenList遍歷一次g值就會+1,但是有可能會有一種情況就是順着一個格子C的周圍的其中一條路徑走了一段發現是錯誤的(即F值不再是OpenList中最小的),這時需要繼續從OpenList中重新找最小F值的item繼續找找新的路徑,假如此時找到了之前C格子周圍的另一個格子的F值最小,此時就需要從這裏重新走新的路徑,新找到的這個格子的G值當然也應該重新變回之前C格子的G+g步長(這個例子中步長爲1),而不是一直自加。下面我會上我修改後的代碼,在修改的地方都有加註釋,你可以去掉源碼的註釋然後註釋掉我的代碼測試一下兩者的區別,我這裏上一下圖,在我之前說的這個例子中兩個代碼的運行結果:
源碼:修改後:
可以看到搜索的路徑差距很大,我的這個執行結果我能看懂,因爲從起點到終點最大的F值只有80,而周圍檢索不到的點F值都達到了100所以不遍歷,但是源碼的這個。。遍歷了這麼大的範圍說實話我是有點蒙圈的,可能是因爲原文章作者的那個路徑比較簡單看不太出問題,我大概想了一會沒想通,就先不糾結啦,反正肯定是和g值這個問題脫不了干係的。
然後貼一下當時實驗的代碼,其實只是整理了一下那篇參考文章的代碼,這裏只關注算法所以並沒有考慮其他東西:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AStarPathFindingTest
{
class Location
{
public int X;
public int Y;
public int F;
public int G;
public int H;
public Location Parent;
}
class Program
{
static string[] map = new string[]
{
"+--------+",
"|00000000|",
"|00000000|",
"|0000X000|",
"|00S0X0E0|",
"|0000X000|",
"|00000000|",
"|00000000|",
"|00000000|",
"+--------+",
};
static void DrawMapToConsole()
{
for (var i = 0; i < map.Length; ++i)
{
Console.WriteLine(map[i]);
}
}
static void Main(string[] args)
{
DrawMapToConsole();
Location current = null;
var start = new Location { X = 3, Y = 4 };
var target = new Location { X = 7, Y = 4 };
var openList = new List<Location>();
var closedList = new List<Location>();
//修改處,g變爲只表示步長
//int g = 0;
int g = 1;
// start by adding the original position to the open list
openList.Add(start);
while (openList.Count > 0)
{
// algorithm's logic goes here
// get the square with the lowest F score
var lowest = openList.Min(l => l.F);
current = openList.First(l => l.F == lowest);
// add the current square to the closed list
closedList.Add(current);
// remove it from the open list
openList.Remove(current);
// show current square on the map
Console.SetCursorPosition(current.X, current.Y);
Console.Write('.');
Console.SetCursorPosition(current.X, current.Y);
///System.Threading.Thread.Sleep(1000);
// if we added the destination to the closed list, we've found a path
if (closedList.FirstOrDefault(l => l.X == target.X && l.Y == target.Y) != null)
break;
var adjacentSquares = GetWalkableAdjacentSquares(current.X, current.Y, map);
//修改處,不能這樣用g
// g++;
foreach (var adjacentSquare in adjacentSquares)
{
// if this adjacent square is already in the closed list, ignore it
if (closedList.FirstOrDefault(l => l.X == adjacentSquare.X
&& l.Y == adjacentSquare.Y) != null)
continue;
// if it's not in the open list...
if (openList.FirstOrDefault(l => l.X == adjacentSquare.X
&& l.Y == adjacentSquare.Y) == null)
{
// compute its score, set the parent
//修改處,不能這樣用g
//adjacentSquare.G = g;
adjacentSquare.G = g + current.G;
adjacentSquare.H = ComputeHScore(adjacentSquare.X,
adjacentSquare.Y, target.X, target.Y);
adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;
adjacentSquare.Parent = current;
// and add it to the open list
openList.Insert(0, adjacentSquare);
}
else
{
// test if using the current G score makes the adjacent square's F score
// lower, if yes update the parent because it means it's a better path
//修改處
if (/*g*/ current.G + g < adjacentSquare.G)
{
//修改處,不能這樣用g
//adjacentSquare.G = g;
adjacentSquare.G = g + current.G;
adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;
adjacentSquare.Parent = current;
}
}
}
}
// assume path was found; let's show it
while (current != null)
{
Console.SetCursorPosition(current.X, current.Y);
Console.Write('_');
Console.SetCursorPosition(current.X, current.Y);
current = current.Parent;
//System.Threading.Thread.Sleep(1000);
}
Console.ReadKey();
}
static List<Location> GetWalkableAdjacentSquares(int x, int y, string[] map)
{
var proposedLocations = new List<Location>()
{
new Location { X = x, Y = y - 1 },
new Location { X = x, Y = y + 1 },
new Location { X = x - 1, Y = y },
new Location { X = x + 1, Y = y },
};
return proposedLocations.Where(
l => map[l.Y][l.X] == '0' || map[l.Y][l.X] == 'E').ToList();
}
static int ComputeHScore(int x, int y, int targetX, int targetY)
{
return Math.Abs(targetX - x) + Math.Abs(targetY - y);
}
}
}
最後說一下上面這些只是最簡單的用曼哈頓距離+4方向實現的A*算法,因爲之前項目也只有4方向,另外還有八方向,斜邊的g值可能會跟平行豎直移動的g值步長不同,例如平行豎直移動g值步長爲10,那斜方向可能爲15。然後還有地形的不同也會影響g步長的不同,這些都算是特殊需求,如果懂了A*算法本身的話做這些修改應該不會很難。還有H值得計算方法除了曼哈頓距離,還有對角線距離,歐幾里得距離等很多計算方法,尋路也分廣度優先和深度優先。還有二叉樹的a*尋路等等等等。。。這些如果到時候做項目的時候有需求我就再整理相關文章。另外還有一篇比較有意思的參考文章,這個系列主要說明和解決了一些使用a*尋路時遇到的一些坑,大概看了一下感覺對以後入坑時會很有幫助,所以一併貼出來。