手把手教会用C++实现A*算法

书接上回,上一篇博客我们介绍了A算法最基本的原理,本篇博客我们来手把手的教会大家A算法的C++实现!原文链接在此,大佬分别用Python、C++、C#进行了算法的实现orz

正文如下:

本文是我对A *的介绍的辅助指南,在此我将解释算法的工作原理。
在此页面上,我将展示如何实现广度优先搜索,Dijkstra的算法,贪婪的最佳优先搜索和A *。 我尝试使代码保持简单。

图搜索有一系列相关算法。算法有很多变体,实现上也有很多变体。将本文展示的代码视为一个起点,而不是适用于所有情况的最终版本。

c++实现

注意:一些示例代码需要 redblobgames/pathfinding/a-star/implementation.cpp才能执行。我使用 C++ 11 编写这些代码,如果你使用的是更老版本的 C++标准,有些代码可能需要更改。

这里的代码仅供教程参考,不具备正式使用质量。本文最后一节会给出一些提示来优化它。

广度优先搜索

让我们在 C++ 中实现广度优先搜索。以下是我们需要用到的组件(与 Python 实现相同):

  • 图(Graph)

    一种数据结构:可以告诉我图中每个位置的相邻位置(参考这篇文章)。加权图还可以告诉我沿着边移动的成本。

  • 位置(Locations)

    一个简单值(整数、字符串、元组等),用于标记图中的位置。它们不一定是具体地图上的某些位置。根据解决的问题,它们还可能包含其他信息,例如方向、燃料、库存等。

  • 搜索(Search)

    一种算法:它接受一个图、一个起始位置以及一个目标位置(可选),并为图中的某些位置甚至所有位置计算出一些有用的信息(是否可访问,它的父指针,之间的距离等)。

  • 队列(Queue)

    搜索算法中用来确定处理图中位置顺序的数据结构。

在之前的介绍文章中,我聚焦在搜索上。在这篇文章中,我会填充余下的细节,来使程序完整。让我们从(Graph)这个数据结构开始,其中位置(节点)为char类型:

struct SimpleGraph {
  std::unordered_map<char, std::vector<char> > edges;

  std::vector<char> neighbors(char id) {
    return edges[id];
  }
};

这里是一个例子:
手把手教会用C++实现A*算法_第1张图片

SimpleGraph example_graph {{
    {'A', {'B'}},
    {'B', {'A', 'C', 'D'}},
    {'C', {'A'}},
    {'D', {'E', 'A'}},
    {'E', {'B'}}
  }};

C++ 标准库中已经包含了队列类,因此,我们现在已经有了图(SimpleGraph)、位置(char)以及队列(std::queue)。那么让我们来尝试一下广度优先搜索:

#include "redblobgames/pathfinding/a-star/implementation.cpp"

void breadth_first_search(SimpleGraph graph, char start) {
  std::queue<char> frontier;
  frontier.push(start);

  std::unordered_set<char> reached;
  reached.insert(start);

  while (!frontier.empty()) {
    char current = frontier.front();
    frontier.pop();

    std::cout << "Visiting " << current << '\n';
    for (char next : graph.neighbors(current)) {
      if (reached.find(next) == reached.end()) {
        frontier.push(next);
        reached.insert(next);
      }
    }
  }
}


int main() {
  breadth_first_search(example_graph, 'A');
}

运行结果:

Visiting A
Visiting B
Visiting C
Visiting D
Visiting E

栅格/网格(Grid)也可以表示为图。现在,我将定义一个名为 SquareGrid 的类,其中的位置数据结构为两个整数。在此地图中,图中的位置(“状态”)与游戏地图上的位置相同,但在许多问题中,图中的位置与地图上的位置不同。我将不显式存储边数据,而是使用neighbors函数来计算它们。不过,在其他许多问题中,最好将它们明确存储:

struct GridLocation {
  int x, y;
};

namespace std {
/* implement hash function so we can put GridLocation into an unordered_set */
template <> struct hash<GridLocation> {
  typedef GridLocation argument_type;
  typedef std::size_t result_type;
  std::size_t operator()(const GridLocation& id) const noexcept {
    return std::hash<int>()(id.x ^ (id.y << 4));
  }
};
}


struct SquareGrid {
  static std::array<GridLocation, 4> DIRS;

  int width, height;
  std::unordered_set<GridLocation> walls;

  SquareGrid(int width_, int height_)
     : width(width_), height(height_) {}

  bool in_bounds(GridLocation id) const {
    return 0 <= id.x && id.x < width
        && 0 <= id.y && id.y < height;
  }

  bool passable(GridLocation id) const {
    return walls.find(id) == walls.end();
  }

  std::vector<GridLocation> neighbors(GridLocation id) const {
    std::vector<GridLocation> results;

    for (GridLocation dir : DIRS) {
      GridLocation next{id.x + dir.x, id.y + dir.y};
      if (in_bounds(next) && passable(next)) {
        results.push_back(next);
      }
    }

    if ((id.x + id.y) % 2 == 0) {
      // aesthetic improvement on square grids
      std::reverse(results.begin(), results.end());
    }

    return results;
  }
};

std::array<GridLocation, 4> SquareGrid::DIRS =
  {GridLocation{1, 0}, GridLocation{0, -1}, GridLocation{-1, 0}, GridLocation{0, 1}};

在辅助文件implementation.cpp我定义了一个函数来生成网格:

#include "redblobgames/pathfinding/a-star/implementation.cpp"

int main() {
  SquareGrid grid = make_diagram1();
  draw_grid(grid, 2);
}

运行结果:

. . . . . . . . . . . . . . . . . . . . . ####. . . . . . . 
. . . . . . . . . . . . . . . . . . . . . ####. . . . . . . 
. . . . . . . . . . . . . . . . . . . . . ####. . . . . . . 
. . . ####. . . . . . . . . . . . . . . . ####. . . . . . . 
. . . ####. . . . . . . . ####. . . . . . ####. . . . . . . 
. . . ####. . . . . . . . ####. . . . . . ##########. . . . 
. . . ####. . . . . . . . ####. . . . . . ##########. . . . 
. . . ####. . . . . . . . ####. . . . . . . . . . . . . . . 
. . . ####. . . . . . . . ####. . . . . . . . . . . . . . . 
. . . ####. . . . . . . . ####. . . . . . . . . . . . . . . 
. . . ####. . . . . . . . ####. . . . . . . . . . . . . . . 
. . . ####. . . . . . . . ####. . . . . . . . . . . . . . . 
. . . . . . . . . . . . . ####. . . . . . . . . . . . . . . 
. . . . . . . . . . . . . ####. . . . . . . . . . . . . . . 
. . . . . . . . . . . . . ####. . . . . . . . . . . . . . .

让我们再实现一遍广度优先搜索,这次带上came_from,对路径持续跟踪:

#include "redblobgames/pathfinding/a-star/implementation.cpp"

template<typename Location, typename Graph>
std::unordered_map<Location, Location>
breadth_first_search(Graph graph, Location start) {
  std::queue<Location> frontier;
  frontier.push(start);

  std::unordered_map<Location, Location> came_from;
  came_from[start] = start;

  while (!frontier.empty()) {
    Location current = frontier.front();
    frontier.pop();

    for (Location next : graph.neighbors(current)) {
      if (came_from.find(next) == came_from.end()) {
        frontier.push(next);
        came_from[next] = current;
      }
    }
  }
  return came_from;
}

int main() {
  SquareGrid grid = make_diagram1();
  auto parents = breadth_first_search(grid, GridLocation{7, 8});
  draw_grid(grid, 2, nullptr, &parents);
}

运行结果:

> > > > v v v v v v v v v v v v < < < < < ####v v v v v v v 
> > > > > v v v v v v v v v v < < < < < < ####v v v v v v v 
> > > > > v v v v v v v v v < < < < < < < ####> v v v v v v 
> > ^ ####v v v v v v v v < < < < < < < < ####> > v v v v v 
> ^ ^ ####v v v v v v v < ####^ < < < < < ####> > > v v v v 
^ ^ ^ ####v v v v v v < < ####^ ^ < < < < ##########v v v < 
^ ^ ^ ####> v v v v < < < ####^ ^ ^ < < < ##########v v < < 
v v v ####> > v v < < < < ####^ ^ ^ ^ < < < < < < < < < < < 
v v v ####> > * < < < < < ####^ ^ ^ ^ ^ < < < < < < < < < < 
v v v ####> ^ ^ ^ < < < < ####^ ^ ^ ^ ^ ^ < < < < < < < < < 
v v v ####^ ^ ^ ^ ^ < < < ####^ ^ ^ ^ ^ ^ ^ < < < < < < < < 
> v v ####^ ^ ^ ^ ^ ^ < < ####^ ^ ^ ^ ^ ^ ^ ^ < < < < < < < 
> > > > > ^ ^ ^ ^ ^ ^ ^ < ####^ ^ ^ ^ ^ ^ ^ ^ ^ < < < < < < 
> > > > ^ ^ ^ ^ ^ ^ ^ ^ ^ ####^ ^ ^ ^ ^ ^ ^ ^ ^ ^ < < < < < 
> > > ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ####^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ < < < <

一些实现使用内部存储(internal storage),创建一个节点(Node)对象来表示图中的节点,通过它来保存came_from和一些其他信息。相反,我选择使用外部存储(external storage),创建一个单独的std::unordered_map来存储图中所有节点的came_from数据。如果你能确定你的图只使用整数来索引节点,那么你还可以使用一维或二维数组 array(或向量 vector)来存储came_from或其他信息。

提前退出

广度优先搜索和 Dijkstra 算法默认会探索整张地图。如果我们只是寻找某一个目标点,我们可以增加一个判断if (current == goal),一旦我们找到目标点,便立即退出循环。

#include "redblobgames/pathfinding/a-star/implementation.cpp"

template<typename Location, typename Graph>
std::unordered_map<Location, Location>
breadth_first_search(Graph graph, Location start, Location goal) {
  std::queue<Location> frontier;
  frontier.push(start);

  std::unordered_map<Location, Location> came_from;
  came_from[start] = start;

  while (!frontier.empty()) {
    Location current = frontier.front();
    frontier.pop();

    if (current == goal) {
      break;
    }
    
    for (Location next : graph.neighbors(current)) {
      if (came_from.find(next) == came_from.end()) {
        frontier.push(next);
        came_from[next] = current;
      }
    }
  }
  return came_from;
}

int main() {
  GridLocation start{8, 7};
  GridLocation goal{17, 2};
  SquareGrid grid = make_diagram1();
  auto came_from = breadth_first_search(grid, start, goal);
  draw_grid(grid, 2, nullptr, &came_from);
}

运行结果:

. > > > v v v v v v v v v v v v < . . . . ####. . . . . . . 
> > > > > v v v v v v v v v v < < < . . . ####. . . . . . . 
> > > > > v v v v v v v v v < < < < . . . ####. . . . . . . 
> > ^ ####v v v v v v v v < < < < < < . . ####. . . . . . . 
. ^ ^ ####> v v v v v v < ####^ < < . . . ####. . . . . . . 
. . ^ ####> > v v v v < < ####^ ^ . . . . ##########. . . . 
. . . ####> > > v v < < < ####^ . . . . . ##########. . . . 
. . . ####> > > * < < < < ####. . . . . . . . . . . . . . . 
. . . ####> > ^ ^ ^ < < < ####. . . . . . . . . . . . . . . 
. . v ####> ^ ^ ^ ^ ^ < < ####. . . . . . . . . . . . . . . 
. v v ####^ ^ ^ ^ ^ ^ ^ < ####. . . . . . . . . . . . . . . 
> v v ####^ ^ ^ ^ ^ ^ ^ ^ ####. . . . . . . . . . . . . . . 
> > > > > ^ ^ ^ ^ ^ ^ ^ ^ ####. . . . . . . . . . . . . . . 
> > > > ^ ^ ^ ^ ^ ^ ^ ^ ^ ####. . . . . . . . . . . . . . . 
. > > ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ####. . . . . . . . . . . . . . .

从结果来看可以发现,现在算法并没有探索整张地图,而是提前结束了。

Dijkstra 算法

下面要开始增加图搜索算法的复杂度了,因为我们将使用比“先进先出(FIFO)”更合适的顺序来处理图中的位置。那么我们需要改变什么呢?

  1. 图(Graph)需要知道移动成本
  2. 队列(Queue)需要以不同之前的顺序返回节点
  3. 搜索算法需要从图中读取成本,并跟踪计算成本,将其给到队列

加权图

一个普通的图告诉我们每个节点的邻居。一个加权图还告诉我们沿着每条边进行移动的成本。我将添加一个cost(from_node, to_node)函数,该函数告诉我们从位置from_node到to_node之间的成本。在此文使用的图中,为了简化,我选择使运动成本仅依赖于to_node,但是还有其他类型,它们的移动成本依赖于from_node和to_node。另一种实现是将移动成本合并到neighbors函数中。这里是一个网格图,其中有一些节点的移动成本是5:

struct GridWithWeights: SquareGrid {
  std::unordered_set<GridLocation> forests;
  GridWithWeights(int w, int h): SquareGrid(w, h) {}
  double cost(GridLocation from_node, GridLocation to_node) const {
    return forests.find(to_node) != forests.end()? 5 : 1;
  }
};

优先队列(Queue with priorities)

我们需要优先队列。C++ 提供了一个priority_queue类,它使用了二叉堆,因此不能为元素重新设置优先级。我将使用 pair(priority,item)作为队列元素,进行排序。C++ 优先队列默认返回优先级最大的元素,使用的是std::less比较符,但是,我们需要的是最小的元素,因此,使用std::greater比较符。

template<typename T, typename priority_t>
struct PriorityQueue {
  typedef std::pair<priority_t, T> PQElement;
  std::priority_queue<PQElement, std::vector<PQElement>,
                 std::greater<PQElement>> elements;

  inline bool empty() const {
     return elements.empty();
  }

  inline void put(T item, priority_t priority) {
    elements.emplace(priority, item);
  }

  T get() {
    T best_item = elements.top().second;
    elements.pop();
    return best_item;
  }
};

在上面的代码中,我对 C++ std::priority_queue 封装了一层,但我认为直接使用该类也很合理。

搜索

这里可以查看地图

template<typename Location, typename Graph>
void dijkstra_search
  (Graph graph,
   Location start,
   Location goal,
   std::unordered_map<Location, Location>& came_from,
   std::unordered_map<Location, double>& cost_so_far)
{
  PriorityQueue<Location, double> frontier;
  frontier.put(start, 0);

  came_from[start] = start;
  cost_so_far[start] = 0;
  
  while (!frontier.empty()) {
    Location current = frontier.get();

    if (current == goal) {
      break;
    }

    for (Location next : graph.neighbors(current)) {
      double new_cost = cost_so_far[current] + graph.cost(current, next);
      if (cost_so_far.find(next) == cost_so_far.end()
          || new_cost < cost_so_far[next]) {
        cost_so_far[next] = new_cost;
        came_from[next] = current;
        frontier.put(next, new_cost);
      }
    }
  }
}

成本变量的类型应完全与图中的类型匹配。如果你使用int,则你可以将int作为成本变量类型以及优先级队列中的优先级;如果你使用double,则成本变量类型和优先级队列中的优先级应相应使用double,诸如此类。在这段代码中,我使用了double,但我也可以使用int,它们的工作原理是相同的。但是,如果你的边成本是浮点型,或者你的启发函数是浮点型,那么在这里你也必须使用浮点型。

最终,在搜素完成后,我们需要构建路径:

template<typename Location>
std::vector<Location> reconstruct_path(
   Location start, Location goal,
   std::unordered_map<Location, Location> came_from
) {
  std::vector<Location> path;
  Location current = goal;
  while (current != start) {
    path.push_back(current);
    current = came_from[current];
  }
  path.push_back(start); // optional
  std::reverse(path.begin(), path.end());
  return path;
}

尽管最好将路径视为由一系列边组成,不过这里将它们存储为一系列节点更为方便。为了构建路径,我们需要从目标节点开始,依据存储在came_from里的数据——指向该节点的前一个节点,不断回溯。当我们到达开始节点时,我们就完成了。不过可以发现,这样构建的路径是反向的(从目标节点到开始节点),因此,如果你想把数据改成正向的,需要在reconstruct_path函数最后调用reverse()方法,将路径反转。有时反向存储更有利,有时将开始节点也加入队列中更有利。

让我们试试看:

#include “redblobgames/pathfinding/a-star/implementation.cpp”

int main() {
  GridWithWeights grid = make_diagram4();
  GridLocation start{1, 4};
  GridLocation goal{8, 5};
  std::unordered_map<GridLocation, GridLocation> came_from;
  std::unordered_map<GridLocation, double> cost_so_far;
  dijkstra_search(grid, start, goal, came_from, cost_so_far);
  draw_grid(grid, 2, nullptr, &came_from);
  std::cout << '\n';
  draw_grid(grid, 3, &cost_so_far, nullptr);
  std::cout << '\n';
  std::vector<GridLocation> path = reconstruct_path(start, goal, came_from);
  draw_grid(grid, 3, nullptr, nullptr, &path);
}

运行结果:

v v < < < < < < < < 
v v < < < ^ ^ < < < 
v v < < < < ^ ^ < < 
v v < < < < < ^ ^ < 
> * < < < < < > ^ < 
^ ^ < < < < . v ^ . 
^ ^ < < < < < v < . 
^ ######^ < v v < . 
^ ######v v v < < < 
^ < < < < < < < < < 

5  4  5  6  7  8  9  10 11 12 
4  3  4  5  10 13 10 11 12 13 
3  2  3  4  9  14 15 12 13 14 
2  1  2  3  8  13 18 17 14 15 
1  0  1  6  11 16 21 20 15 16 
2  1  2  7  12 17 .  21 16 .  
3  2  3  4  9  14 19 16 17 .  
4  #########14 19 18 15 16 .  
5  #########15 16 13 14 15 16 
6  7  8  9  10 11 12 13 14 15 

.  @  @  @  @  @  @  .  .  .  
.  @  .  .  .  .  @  @  .  .  
.  @  .  .  .  .  .  @  @  .  
.  @  .  .  .  .  .  .  @  .  
.  @  .  .  .  .  .  .  @  .  
.  .  .  .  .  .  .  .  @  .  
.  .  .  .  .  .  .  .  .  .  
.  #########.  .  .  .  .  .  
.  #########.  .  .  .  .  .  

结果与 Python 版本并不完全相同,因为我使用的都是它们内置的优先级队列,而 C++ 和 Python 可能会对相同优先级的节点进行不同的排序。

A*搜索

A* 算法几乎和 Dijkstra 算法一样,只不过我们添加了一个启发函数。请注意,该算法并不只针对网格地图。关于网格的信息在图类型(SquareGrids)、位置类(Location结构)以及heuristic函数中。将它们3个替换掉,你可以将 A* 算法与任意其他图数据结构结合在一起使用。

inline double heuristic(GridLocation a, GridLocation b) {
  return std::abs(a.x - b.x) + std::abs(a.y - b.y);
}

template<typename Location, typename Graph>
void a_star_search
  (Graph graph,
   Location start,
   Location goal,
   std::unordered_map<Location, Location>& came_from,
   std::unordered_map<Location, double>& cost_so_far)
{
  PriorityQueue<Location, double> frontier;
  frontier.put(start, 0);

  came_from[start] = start;
  cost_so_far[start] = 0;
  
  while (!frontier.empty()) {
    Location current = frontier.get();

    if (current == goal) {
      break;
    }

    for (Location next : graph.neighbors(current)) {
      double new_cost = cost_so_far[current] + graph.cost(current, next);
      if (cost_so_far.find(next) == cost_so_far.end()
          || new_cost < cost_so_far[next]) {
        cost_so_far[next] = new_cost;
        double priority = new_cost + heuristic(next, goal);
        frontier.put(next, priority);
        came_from[next] = current;
      }
    }
  }
}

priority值的类型,包括优先级队列中使用的类型,必须最够大,大到可以包含图中移动成本(cost_t)和启发函数值。例如,如果图成本为整数,且启发函数返回双精度,则优先级队列也必须接受双精度值。在上述示例代码中,我对这三个值(成本费用,启发式函数和优先级)统一使用了double,不过我也可以使用int,因为我的费用成本和启发式函数都是整数值。

小提示:写frontier.put(start, heuristic(start, goal))比写frontier.put(start, 0)更正确,但是这里没有区别,因为起始节点的优先级无关紧要。开始时,它是优先级队列中唯一的节点,一定会被优先访问并从队列中删除。

#include "redblobgames/pathfinding/a-star/implementation.cpp"

int main() {
  GridWithWeights grid = make_diagram4();
  GridLocation start{1, 4};
  GridLocation goal{8, 5};
  std::unordered_map<GridLocation, GridLocation> came_from;
  std::unordered_map<GridLocation, double> cost_so_far;
  a_star_search(grid, start, goal, came_from, cost_so_far);
  draw_grid(grid, 2, nullptr, &came_from);
  std::cout << '\n';
  draw_grid(grid, 3, &cost_so_far, nullptr);
  std::cout << '\n';
  std::vector<GridLocation> path = reconstruct_path(start, goal, came_from);
  draw_grid(grid, 3, nullptr, nullptr, &path);
}

运行结果

v v v v < < < < < < 
v v v v < ^ ^ < < < 
v v v v < < ^ ^ < < 
v v v < < < . ^ ^ < 
> * < < < < . > ^ < 
> ^ < < < < . . ^ . 
^ ^ ^ < < < . . . . 
^ ######^ . . . . . 
^ ######. . . . . . 
^ . . . . . . . . . 

5  4  5  6  7  8  9  10 11 12 
4  3  4  5  10 13 10 11 12 13 
3  2  3  4  9  14 15 12 13 14 
2  1  2  3  8  13 .  17 14 15 
1  0  1  6  11 16 .  20 15 16 
2  1  2  7  12 17 .  .  16 .  
3  2  3  4  9  14 .  .  .  .  
4  #########14 .  .  .  .  .  
5  #########.  .  .  .  .  .  
6  .  .  .  .  .  .  .  .  .  

.  .  .  @  @  @  @  .  .  .  
.  .  .  @  .  .  @  @  .  .  
.  .  .  @  .  .  .  @  @  .  
.  .  @  @  .  .  .  .  @  .  
.  @  @  .  .  .  .  .  @  .  
.  .  .  .  .  .  .  .  @  .  
.  .  .  .  .  .  .  .  .  .  
.  #########.  .  .  .  .  .  
.  #########.  .  .  .  .  .  
.  .  .  .  .  .  .  .  .  .

更直接的路径

如果你在自己的项目中实现这些代码,可能会发现某些路径并不如你所愿。这是正常的。当使用网格(grid)时,特别是在移动成本都相同的网格中,最终会遇到一些局限:很多条路径的成本是完全相同的。A* 最终选择了这些路径中的一条,但这通常对你来说并不是最合适的路径。有个快速技巧可以打破这个局限,但并不能达到完美。更好的方式是改变地图的表现形式,这不仅可以使 A* 运行的更快,还可以产生更直接、美观的路径。然而,这些仅适用于移动成本相同的大部分静态地图。对于上文的演示,我使用了快速技巧,但也仅适用于优先级较低的队列。如果你使用的是优先级较高的队列,那么你需要使用其他的技巧。

产品代码

上面展示的 C++ 代码经过简化,可以更轻松地展示算法和数据结构。然而,在实践中,你需要做很多不同的事情:

  • 内联(inline)短小函数
  • Location参数应该是Graph的一部分
  • 费用成本可以为int或double,并且应该是Graph的一部分
  • 如果图节点索引是密集整数,使用array而不是unordered_set,并且在退出时重制这些值,而不是在进入的时候初始化
  • 使用引用传递内存大的数据结构,而不是值传递
  • 通过参数(out parameters)返回内存大的数据结构,而不是直接return它,或者使用移动构造函数(move constructors)(举个例子,neighbors函数返回vector)
  • 启发式函数易变化,应该成为 A* 函数的一个模版参数,这样可以进行内联

以下函数展示了上述这些变化(但不是全部):

template<typename Graph>
void a_star_search
  (Graph graph,
   typename Graph::Location start,
   typename Graph::Location goal,
   std::function<typename Graph::cost_t(typename Graph::Location a, typename Graph::Location b)> heuristic,
   std::unordered_map<typename Graph::Location, typename Graph::Location>& came_from,
   std::unordered_map<typename Graph::Location, typename Graph::cost_t>& cost_so_far)
{
  typedef typename Graph::Location Location;
  typedef typename Graph::cost_t cost_t;
  PriorityQueue<Location, cost_t> frontier;
  std::vector<Location> neighbors;
  frontier.put(start, cost_t(0));

  came_from[start] = start;
  cost_so_far[start] = cost_t(0);
  
  while (!frontier.empty()) {
    typename Location current = frontier.get();

    if (current == goal) {
      break;
    }

    graph.get_neighbors(current, neighbors);
    for (Location next : neighbors) {
      cost_t new_cost = cost_so_far[current] + graph.cost(current, next);
      if (cost_so_far.find(next) == cost_so_far.end()
          || new_cost < cost_so_far[next]) {
        cost_so_far[next] = new_cost;
        cost_t priority = new_cost + heuristic(next, goal);
        frontier.put(next, priority);
        came_from[next] = current;
      }
    }
  }
}

我希望本文代码更多展示数据结构和算法,而不是展示 C++ 优化,所以我努力简化代码,不是写运行更快或更抽象的代码。

算法变体

本文中的 Dijkstra 算法和 A* 算法与常见 AI 课本上的算法略有不同。

纯粹的 Dijkstra 算法会将全部节点添加到优先级队列开始,并且没有提前退出逻辑。它在队列中使用“减小键(decrease-key)”的操作。理论上没问题,但是在实践中…

  1. 通过启动时优先级队列只包含起始节点,我们可以保证队列很小,这可以提高运行效率、减小内存使用。
  2. 使用提前退出,我们几乎不需要将所有节点都插入优先级队列中,并且一旦我们找到目标立即返回路径。
  3. 通过一开始不将所有节点都加入队列中,大多数时间我们可以使用性能更好的插入操作,而不是性能更差的减小键(decrease-key)操作。
  4. 通过一开始不将所有节点都加入队列中,我们可以处理无法得知图中所有节点,或者图中由无穷多个节点这些情况。

这个变体有时被称为“统一成本搜索(Uniform Cost Search)”。请参阅维基百科以查看伪代码,或阅读Felner的论文[pdf]来查看这些更改的依据。
我的版本与你可能在其他地方找到的版本之间还有三个不同之处。这些变化适用于 Dijkstra 算法和 A* 算法:

  1. 我省去了检查节点是否处于边界队列中且成本更高。这样,我最终在边界队列中得到了重复的元素。不过该算法仍然有效。它将重新访问某些必要位置(但根据我的经验,只要启发函数合理,很少出现这种情况)。这样代码更为简单,它也使我可以使用更简单、更快速的优先级队列,尽管该队列不支持减少键操作。论文“优先级队列和Dijkstra 的算法”表明,这种方法在实践中速度更快。
  2. 我没有存储“封闭集(closed set)”和“开放集(open set)”,而是用一个visited来告诉我它是否在这些集合中。这进一步简化了代码。
  3. 我不需要单独显式存储的开放集或封闭集合,因为came_from和cost_so_far表的键隐式包含了这些信息。由于我们总是需要这两个表之一,因此无需再分别存储开放/封闭集合。
  4. 我使用哈希表而不是节点对象的数组。这消除了许多其他实现中相当耗时的初始化步骤。对于大型游戏地图,这些数组的初始化通常比 A* 的其余部分慢。

如果你有更多建议可以简化并保持性能,请让我知道!

你可能感兴趣的:(学习笔记,移动机器人运动规划,翻译)