Graph/Dijkstra.hpp: Shortest-path-tree algorithm.

This commit is contained in:
ZmnSCPxj jxPCSnmZ 2020-10-02 12:10:52 +08:00
parent 71b03f42fb
commit bb23e2780f
4 changed files with 385 additions and 0 deletions

244
Graph/Dijkstra.hpp Normal file
View file

@ -0,0 +1,244 @@
#ifndef GRAPH_DIJKSTRA_HPP
#define GRAPH_DIJKSTRA_HPP
#include"Graph/TreeNode.hpp"
#include"Util/make_unique.hpp"
#include<algorithm>
#include<functional>
#include<map>
#include<memory>
#include<queue>
#include<set>
namespace Graph {
/** class Graph::Dijkstra<Node, Cost>
*
* @brief an implementation of the Dijkstra
* shortest-path algorithm.
*
* @desc this implementation uses a data
* interface, i.e. it is mostly passive and
* will just wait for data to be fed into it
* by the client.
*
* This allows clients with non-standard
* execution policies and incremental map
* loading to use this class.
*/
template< typename Node
, typename Cost = double
, typename CmpN = std::less<Node>
, typename CmpC = std::less<Cost>
, typename AddC = std::plus<Cost>
>
class Dijkstra {
public:
typedef Graph::TreeNode<std::pair<Node const*, Cost>> TreeNode;
typedef
std::map< Node
, std::unique_ptr<TreeNode>
, CmpN
> Result;
Dijkstra() =delete;
Dijkstra(Dijkstra&&) =default;
Dijkstra( Dijkstra const& o
) : cmp_c(o.cmp_c)
, add_c(o.add_c)
, q(o.q)
, treenodes()
, closed(o.closed)
{
/* treenodes have to be copied. */
copy_treenodes(o.treenodes);
}
explicit
Dijkstra( Node root
, Cost root_cost = 0
, CmpN cmp_n_ = CmpN()
, CmpC cmp_c_ = CmpC()
, AddC add_c_ = AddC()
) : cmp_c(cmp_c_)
, add_c(std::move(add_c_))
, q(WrappedCmpC(cmp_c_))
, treenodes(cmp_n_)
, closed()
{
initialize(std::move(root), std::move(root_cost));
}
/** Graph::Dijkstra<Node, Cost>::current
*
* @brief return the current node being considered
* by the algorithm.
*
* @desc the client should provide the neighbors
* of the current node via the `neighbor` member
* function.
*
* This function can return `nullptr`, meaning the
* run has ended and there are no more nodes
* reachable from the root node.
*
* If you are using this class as a pathfinding
* algorithm (as opposed to a shortest-path-tree
* algorithm) you can stop when the goal node (or
* one of the goal nodes) is returned by this
* function.
*/
Node const* current() const {
if (q.empty())
return nullptr;
return q.top().first->data.first;
}
/** Graph::Dijsktra<Node, Cost>::neighbor
*
* @brief inform the algorithm that the `current()`
* node has a directly-reachable neighbor, that has
* a cost `c` to go to.
*
* @desc do not call this if `current()` returns
* `nullptr`.
* Calling this will not change what `current()`
* returns.
*/
void neighbor(Node n, Cost c) {
/* for each neighbor v of u: */ /* v = n, u = current() */
auto& u = *q.top().first;
auto& u_cost = u.data.second;
auto alt = add_c(u_cost, c);
auto it = treenodes.find(n);
if (it == treenodes.end()) {
/* No TreeNode yet, initialize it to alt. */
auto& v = create_treenode(n);
v.data.second = alt;
reparent(v, u);
q.push(std::make_pair(&v, alt));
} else {
auto& v = *it->second;
if (cmp_c(alt, v.data.second)) {
v.data.second = alt;
reparent(v, u);
q.push(std::make_pair(&v, alt));
}
}
}
/** Graph::Dijkstra<Node, Cost>::end_neighbors
*
* @brief inform the algorithm that the `current()`
* node has no more neighbors.
*
* @desc do not call this if `current()` returns
* `nullptr`.
* After this call, `current()` will change.
*/
void end_neighbors() {
auto u = q.top().first;
closed.insert(u);
q.pop();
/* Filter out those already in the closed set. */
while ( !q.empty()
&& closed.find(q.top().first) != closed.end()
)
q.pop();
}
/** Graph::Dijkstra<Node::Cost>::finalize
*
* @brief destruct this algorithm and extract its
* result.
*
* @desc after calling this function, the object is
* now in an unusable state.
*/
Result
finalize()&& {
return std::move(treenodes);
}
private:
class WrappedCmpC {
private:
CmpC cmp_c;
public:
explicit
WrappedCmpC(CmpC cmp_c_) : cmp_c(cmp_c_) { }
bool operator()( std::pair<TreeNode*, Cost> const& a
, std::pair<TreeNode*, Cost> const& b
) const {
/* We want the priority queue to return
* the *lowest* cost, but std::priority_queue
* is designed to return the *highest* score
* when given std::less.
* So flip the args here.
*/
return cmp_c(b.second, a.second);
}
};
CmpC cmp_c;
AddC add_c;
std::priority_queue< std::pair<TreeNode*, Cost>
, std::vector<std::pair<TreeNode*, Cost>>
, WrappedCmpC
> q;
Result treenodes;
std::set<TreeNode*> closed;
void initialize(Node root, Cost root_cost) {
auto& tn = get_treenode(root);
tn.data.second = root_cost;
q.push(std::make_pair(&tn, root_cost));
}
TreeNode& get_treenode(Node n) {
auto it = treenodes.find(n);
if (it == treenodes.end())
return create_treenode(std::move(n));
return *it->second;
}
TreeNode& create_treenode(Node n) {
auto tn = Util::make_unique<TreeNode>();
auto itb = treenodes.insert(std::make_pair(
std::move(n), std::move(tn)
));
auto it = itb.first;
it->second->data.first = &it->first;
return *it->second;
}
void reparent(TreeNode& node, TreeNode& parent) {
if (node.parent) {
/* Remove it from the existing parent children. */
auto& curparent = const_cast<TreeNode&>(*node.parent);
auto& children = curparent.children;
children.erase( std::remove( children.begin()
, children.end()
, &node
)
, children.end()
);
}
node.parent = &parent;
parent.children.push_back(&node);
}
void copy_treenodes(Result const& src) {
for (auto const& e : src) {
auto const& n = e.first;
auto const& tn = *e.second;
auto& my_tn = get_treenode(n);
if (tn.parent) {
auto const& parent_n = *tn.parent->data.first;
auto& my_parent_tn = get_treenode(parent_n);
reparent(my_tn, my_parent_tn);
}
my_tn.data.second = tn.data.second;
}
}
};
}
#endif /* !defined(GRAPH_DIJKSTRA_HPP) */

22
Graph/TreeNode.hpp Normal file
View file

@ -0,0 +1,22 @@
#ifndef GRAPH_TREENODE_HPP
#define GRAPH_TREENODE_HPP
#include<vector>
namespace Graph {
/** struct Graph::TreeNode<a>
*
* @brief generic n-ary tree node, with
* associated data on the nodes.
*/
template<typename a>
struct TreeNode {
a data;
TreeNode const* parent;
std::vector<TreeNode const*> children;
};
}
#endif /* !defined(GRAPH_TREENODE_HPP) */

View file

@ -247,6 +247,8 @@ libclboss_la_SOURCES = \
Ev/start.hpp \
Ev/yield.cpp \
Ev/yield.hpp \
Graph/Dijkstra.hpp \
Graph/TreeNode.hpp \
Jsmn/Detail/EndAdvancer.cpp \
Jsmn/Detail/EndAdvancer.hpp \
Jsmn/Detail/ParseResult.hpp \
@ -390,6 +392,7 @@ TESTS = \
tests/ev/test_plus \
tests/ev/test_runcmd \
tests/ev/test_throw_in_then \
tests/graph/test_dijkstra \
tests/json/test_out_simple \
tests/ln/test_amount \
tests/ln/test_nodeid \

View file

@ -0,0 +1,116 @@
#undef NDEBUG
#include"Graph/Dijkstra.hpp"
#include"Ln/NodeId.hpp"
#include<assert.h>
namespace {
auto const A = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000000");
auto const B = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000001");
auto const C = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000002");
template<typename a>
void test_copy(Graph::Dijkstra<a> const& dijkstra) {
auto copy = dijkstra;
if (dijkstra.current()) {
assert(copy.current());
assert(*copy.current() == *dijkstra.current());
} else {
assert(!copy.current());
}
}
}
int main() {
auto dijkstra = Graph::Dijkstra<Ln::NodeId>(A);
assert(dijkstra.current());
assert(*dijkstra.current() == A);
test_copy(dijkstra);
{
/* We can finalize at any time, at the cost of
* creating a copy of the shortest-path tree. */
auto tmp = Graph::Dijkstra<Ln::NodeId>(dijkstra).finalize();
assert(tmp.size() == 1);
assert(tmp.find(A) != tmp.end());
assert(tmp[A]->data.second == 0);
}
/*
* A --5-- C
* \ /
* 1 1
* \ /
* B
*/
dijkstra.neighbor(B, 1);
dijkstra.neighbor(C, 5);
dijkstra.end_neighbors();
{
auto tmp = Graph::Dijkstra<Ln::NodeId>(dijkstra).finalize();
assert(tmp.size() == 3);
assert(tmp.find(A) != tmp.end());
assert(tmp.find(B) != tmp.end());
assert(tmp.find(C) != tmp.end());
assert(tmp[A]->data.second == 0);
assert(tmp[B]->data.second == 1);
assert(tmp[C]->data.second == 5);
assert(!tmp[A]->parent);
assert(*tmp[B]->parent->data.first == A);
assert(*tmp[C]->parent->data.first == A);
}
/* B is nearer, so it should come first. */
assert(dijkstra.current());
assert(*dijkstra.current() == B);
/* Give its neighbors. */
dijkstra.neighbor(A, 1);
dijkstra.neighbor(C, 1);
dijkstra.end_neighbors();
{
auto tmp = Graph::Dijkstra<Ln::NodeId>(dijkstra).finalize();
assert(tmp.size() == 3);
assert(tmp.find(A) != tmp.end());
assert(tmp.find(B) != tmp.end());
assert(tmp.find(C) != tmp.end());
assert(tmp[A]->data.second == 0);
assert(tmp[B]->data.second == 1);
/* C should now be reached by route A->B->C. */
assert(tmp[C]->data.second == 2);
assert(!tmp[A]->parent);
assert(*tmp[B]->parent->data.first == A);
assert(*tmp[C]->parent->data.first == B);
}
/* C should now be what is queried. */
assert(dijkstra.current());
assert(*dijkstra.current() == C);
/* Give its neighbors. */
dijkstra.neighbor(A, 5);
dijkstra.neighbor(B, 1);
dijkstra.end_neighbors();
/* Same result as above. */
{
auto tmp = Graph::Dijkstra<Ln::NodeId>(dijkstra).finalize();
assert(tmp.size() == 3);
assert(tmp.find(A) != tmp.end());
assert(tmp.find(B) != tmp.end());
assert(tmp.find(C) != tmp.end());
assert(tmp[A]->data.second == 0);
assert(tmp[B]->data.second == 1);
assert(tmp[C]->data.second == 2);
assert(!tmp[A]->parent);
assert(*tmp[B]->parent->data.first == A);
assert(*tmp[C]->parent->data.first == B);
}
/* Dijkstra should have completed. */
assert(!dijkstra.current());
return 0;
}