Here is my A* algorithm. I tried hard to implement it generically, but come up only with one idea to use lambdas for heuristic and next nodes (neighbours / successors) functions. I know that using good heuristic function is the key to speed up, but here i'm trying to squeeze everything from current implementation not taking into account any external factors.
/**
* @brief generic a* algorithm
*
* @tparam T any type with operator< and operator==
* @tparam Heuristic function, returns uint32_t
* @tparam Mutator function returns iterable of all next states of node
* @param initial initial state of node
* @param expected expected state of node
* @param heuristic function like manhattan or euclidean distance
* @param next next mutator
* @return std::vector<T> representing path from expected to initial
*/
template <class T, class Heuristic, class Mutator>
std::vector<T> path(const T& initial, const T& expected,
Heuristic heuristic, Mutator next) {
struct Node {
using ref = std::shared_ptr<Node>;
explicit Node(const T& data, ref parent, uint32_t g, uint32_t h)
: data(data), parent(parent), g(g), h(h), f(g + h) {}
const T data; // actual data
const ref parent{}; // pointer to parent
// scores
const uint32_t g, h, f;
};
static auto path = [](auto n) {
std::vector<T> result;
for (; n; n = n->parent)
result.push_back(n->data);
return result;
};
// comparator for nodes::ref's to make heap
static auto comp = [](auto&& lhs, auto&& rhs) { return lhs->f > rhs->f; };
// std::vector<T> closed;
std::set<T> closed; // <- required operator<
// vector will be used as priority_queue
std::vector<typename Node::ref> opened;
// initial state with g score = 0
opened.push_back(std::make_shared<Node>(initial, nullptr, 0,
heuristic(initial, expected)));
while (not opened.empty()) {
// pop opened into curr
const auto curr = opened.front();
// if goal found
if (curr->data == expected) return path(curr);
// poping priority queue
std::pop_heap(opened.begin(), opened.end(), comp);
opened.pop_back();
// foreach node in successors
for (auto&& child : next(curr->data)) {
// if was in closed
if (!closed.insert(child).second) continue;
// setting up child node
const auto node = std::make_shared<Node>(child, curr, curr->g + 1,
heuristic(child, expected));
// find the node with the same data as child
auto it = std::find_if(opened.begin(), opened.end(), [&child](auto&& node) {
return node->data == child;
});
// if opened does not contains child data push and continue
if (it == opened.end()) {
opened.push_back(std::move(node));
std::push_heap(opened.begin(), opened.end(), comp);
continue;
}
// if current child node is better than found node
// replace previos node with current
if (node->f < (*it)->f) *it = node;
}
}
return {}; // impossible to reach
}
Is there any way to improve performance?
not
operator confused me for a second until I remembered about alternative operators. I'm not sure if it's considered good practice to use them? \$\endgroup\$while (not opened.empty())
I would have expected to see the same inif (!closed.insert(child).second)
. \$\endgroup\$