From bb23e2780f6b649ba460452013cda13e254f0c1f Mon Sep 17 00:00:00 2001 From: ZmnSCPxj jxPCSnmZ Date: Fri, 2 Oct 2020 12:10:52 +0800 Subject: [PATCH] Graph/Dijkstra.hpp: Shortest-path-tree algorithm. --- Graph/Dijkstra.hpp | 244 ++++++++++++++++++++++++++++++++++ Graph/TreeNode.hpp | 22 +++ Makefile.am | 3 + tests/graph/test_dijkstra.cpp | 116 ++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 Graph/Dijkstra.hpp create mode 100644 Graph/TreeNode.hpp create mode 100644 tests/graph/test_dijkstra.cpp diff --git a/Graph/Dijkstra.hpp b/Graph/Dijkstra.hpp new file mode 100644 index 0000000..7729aa9 --- /dev/null +++ b/Graph/Dijkstra.hpp @@ -0,0 +1,244 @@ +#ifndef GRAPH_DIJKSTRA_HPP +#define GRAPH_DIJKSTRA_HPP + +#include"Graph/TreeNode.hpp" +#include"Util/make_unique.hpp" +#include +#include +#include +#include +#include +#include + +namespace Graph { + +/** class Graph::Dijkstra + * + * @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 + , typename CmpC = std::less + , typename AddC = std::plus + > +class Dijkstra { +public: + typedef Graph::TreeNode> TreeNode; + typedef + std::map< Node + , std::unique_ptr + , 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::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::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::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::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 const& a + , std::pair 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 + , std::vector> + , WrappedCmpC + > q; + Result treenodes; + std::set 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(); + 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(*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) */ diff --git a/Graph/TreeNode.hpp b/Graph/TreeNode.hpp new file mode 100644 index 0000000..78a1459 --- /dev/null +++ b/Graph/TreeNode.hpp @@ -0,0 +1,22 @@ +#ifndef GRAPH_TREENODE_HPP +#define GRAPH_TREENODE_HPP + +#include + +namespace Graph { + +/** struct Graph::TreeNode + * + * @brief generic n-ary tree node, with + * associated data on the nodes. + */ +template +struct TreeNode { + a data; + TreeNode const* parent; + std::vector children; +}; + +} + +#endif /* !defined(GRAPH_TREENODE_HPP) */ diff --git a/Makefile.am b/Makefile.am index 1eedd27..4c39fce 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/tests/graph/test_dijkstra.cpp b/tests/graph/test_dijkstra.cpp new file mode 100644 index 0000000..39998c4 --- /dev/null +++ b/tests/graph/test_dijkstra.cpp @@ -0,0 +1,116 @@ +#undef NDEBUG +#include"Graph/Dijkstra.hpp" +#include"Ln/NodeId.hpp" +#include + +namespace { + +auto const A = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000000"); +auto const B = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000001"); +auto const C = Ln::NodeId("020000000000000000000000000000000000000000000000000000000000000002"); + +template +void test_copy(Graph::Dijkstra 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(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(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(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(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(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; +}