几个常用寻路算法的分析与实现。
图形
采用相邻列,每个节点都有一个相邻节点集合。
无权图
| 12
 3
 4
 5
 
 | struct GraphNode{
 
 std::vector<GraphNode*> mAdjacent;
 };
 
 | 
| 12
 3
 4
 5
 
 | struct Graph{
 
 std::vector<GraphNode*> mNodes;
 };
 
 | 
加权图
| 12
 3
 4
 5
 6
 
 | struct WeightedEdge{
 struct WeightedGraphNode* mFrom;
 struct WeightedGraphNode* mTo;
 float mWeight;
 };
 
 | 
| 12
 3
 4
 5
 
 | struct WeightedGraphNode{
 
 std::vector<WeightedEdge*> mEdges;
 };
 
 | 
| 12
 3
 4
 5
 
 | struct Graph{
 
 std::vector<WeightedGraphNode*> mNodes;
 };
 
 | 
广度优先搜索(BFS)
首先检查起始节点一步范围内的所有节点,如果没有目标节点,再检查起始节点两步范围内的所有节点。以此类推,直至找到目标节点,或者再无有效移动为止。
在搜索期间,每一个节点都需要知道前一个被访问节点。那一个节点被称为“父节点”,该节点能够在广度优先搜索完成后,帮助重建路径。虽然可以将这些数据添加到GraphNode结构体中,但最好还是将不会变化(图形本身)的数据与其“父节点”分开。这是因为基于所选的起始和目标节点,父节点将会发生变化。分离这些数据段还意味着,如果想跨多线程同时计算多条路径,搜索就不会相互干扰。
| 1
 | using Node2ParentMap = std::unordered_map<const GraphNode*, const GraphNode*>;
 | 
使用队列来进行搜索。首先,从起始节点入队,并输入一个循环。在每次迭代中,都需要出列一个节点,并入队其相邻节点。可以通过检查“父映射”,来避免多次将同一节点添加到队列中。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | bool BFS(const Graph& graph, const GraphNode* start, const Graph* goal, Node2ParentMap& outMap)
 {
 bool pathFound = false;
 std::queue<cosnt GraphNode*> q;
 q.emplace(start);
 
 while (!q.empty())
 {
 const GraphNode* current = q.front();
 q.pop();
 if (current == goal)
 {
 pathfound = true;
 break;
 }
 
 for (const GraphNode* node: current->mAdjacent)
 {
 const GraphNode* parent = outMap[node];
 if (parent == nullptr && node != start)
 {
 outMap[node] = current;
 q.emplace(node);
 }
 }
 }
 retrun pathFound;
 }
 
 | 
如果BFS成功,就可以使用outMap中的父指针来重建路径。跟随父指针这条链条,将会产生一条目标节点到起始节点的链条,所以应采用反向搜索。
针对加权图,BFS无法保证找到最短路径。这是因为BFS根本不考虑边的权重,对于BFS,每次边的遍历都是等同的。
heuristic函数
在寻路算法中,heuristic函数是从给定节点到目标节点的估计成本。
如果heuristic函数总是小于或等于从节点x到目标节点的实际成本,那么该函数是可接受的;如果heuristic函数偶尔会高过实际成本,那么该函数便是不可接受的。这种情况下,搜索应该放弃使用该函数。
曼哈顿距离
$h(x) = |start.x - end.x| + |start.y- end.y|$
曼哈顿距离假定对角线移动是无效的,如果对角线距离有效,曼哈顿距离则会经常高估成本,使得heuristic函数不可接受
欧几里德距离
$h(x) = \sqrt{(start.x - end.x)^2 + (start.y- end.y)^2}$
贪婪最佳优先搜索(GBFS)
和BFS不同,GBFS并不是以FIFO方式使用队列来考虑节点的,而是根据启发(Heuristic)函数来决定接下来考虑哪个节点。
搜索过程中那个,GBFS并不是使用队列,而是使用两组节点。开集包含正在考虑的节点。一旦节点被选用于评估,就会被移动到闭集中。
对于开集,需要进行两个操作:
对与闭集,只需要测试成员身份,所以对闭集不需要采用实际集合。
在搜索过程中,每个节点都需要额外数据。有多个临时数据,采用结构体:
| 12
 3
 4
 5
 6
 7
 
 | struct GBFSScratch{
 const WeightedEdge* mParentEdge = nullptr;
 float mHeuristic = 0.0f;
 bool mInOpenSet = false;
 bool mInCloseSet = false;
 }
 
 | 
定义一个映射:
| 1
 | using GBFSMap = std::unordered_map<const WeightedGraphNode*, GBFSScratch>;
 | 
完整代码:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 
 | bool GBFS(const WeightedGraph& g, const WeightedGraphNode* start, const WeightedGraphNode* goal, GBFSMap& outMap)
 {
 std::vector<const WeightedGraphNode*> openSet;
 const WeightedGraphNode* current = start;
 outMap[current].mInCloseSet  = True;
 
 while (current != goal)
 {
 for (const WeightedEdge* edge: current.mEdges)
 {
 GBFSScratch& data = outMap[nodeTo];
 if (!data.mInCloseSet)
 {
 data.mParentEdge = current;
 if (!data.mInOpenSet)
 {
 data.mHeuristic = ComputeHeuristic(edge->mTo, goal);
 data.mInOpenSet = True;
 openSet.emplace_back(edge->mTo);
 }
 }
 }
 
 if (openSet.empty())
 {
 break;
 }
 
 
 auto iter = std::min_element(openSet.begin(), openSet.end(),
 [&outMap](const WeightedGraphNode* a, const WeightedGraphNode* b)
 {
 return outMap[a].mHeuristic < outMap[a].mHeuristic;
 });
 
 current = iter;
 outMap[current].mInOpenSet = false;
 outMap[current].mInCloseSet = true;
 openSet.erase(current)
 
 }
 return (current == goal) ? true: false;
 }
 
 | 
A*搜索
A*对GBFS做了一些修改,引入了路径成本组件,该组件是从起点到给定节点的实际成本。符号$g(x)$表示节点的路径成本。A*会选择具有最低$f(x)$值的节点:
完整代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 
 | bool AStar(const WeightedGraph& g, const WeightedGraphNode* start, const WeightedGraphNode* goal, AStarMap& outMap)
 {
 std::vector<const WeightedGraphNode*> openSet;
 const WeightedGraphNode* current = start;
 outMap[current].mInCloseSet  = True;
 
 while (current != goal)
 {
 for (const WeightedEdge* edge: current.mEdges)
 {
 WeightedGraphNode& nodeTo = edge->mTo;
 AStarScratch& data = outMap[nodeTo];
 if (!data.mInCloseSet)
 {
 if (!data.mInOpenSet)
 {
 data.mParentEdge = current;
 data.mHeuristic = ComputeHeuristic(nodeTo, goal);
 data.mActualFromStart = outMap[current].mActualFromStart + nodeTo.mWeight;
 data.mInOpenSet = True;
 openSet.emplace_back(nodeTo);
 }
 else {
 float newG = outMap[current].mActualFromStart + nodeTo.mWeight;
 if (newG < data.mActualFromStart)
 {
 data.mParentEdge = current;
 data.mActualFromStart = newG;
 }
 }
 }
 }
 
 if (openSet.empty())
 {
 break;
 }
 
 
 auto iter = std::min_element(openSet.begin(), openSet.end(),
 [&outMap](const WeightedGraphNode* a, const WeightedGraphNode* b)
 {
 
 float fOfA = a.mActualFromStart + a.mHeuristic;
 float fOfB = b.mActualFromStart + b.mHeuristic;
 return fOfA < fOfB;
 });
 
 current = *iter;
 openSet.erase(iter)
 outMap[current].mInOpenSet = false;
 outMap[current].mInCloseSet = true;
 
 }
 return (current == goal) ? true: false;
 }
 
 |