From 6d8b771545255c6acfbecfe31dc5ffc2e1848d31 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Sun, 29 Jun 2025 00:56:28 +0200 Subject: [PATCH 1/2] generalize Dijkstra's shortest paths to non-index adjacency lists --- .../graph/algorithm/common_shortest_paths.hpp | 2 + include/graph/algorithm/dijkstra_clrs.hpp | 2 + .../algorithm/dijkstra_shortest_paths.hpp | 169 +++++++++++------- include/graph/detail/graph_cpo.hpp | 100 ++++++++++- include/graph/graph.hpp | 16 ++ tests/CMakeLists.txt | 1 + tests/map_adjacency_list_tests.cpp | 135 ++++++++++++++ 7 files changed, 356 insertions(+), 69 deletions(-) create mode 100644 tests/map_adjacency_list_tests.cpp diff --git a/include/graph/algorithm/common_shortest_paths.hpp b/include/graph/algorithm/common_shortest_paths.hpp index 1122a81..bf5f9fc 100644 --- a/include/graph/algorithm/common_shortest_paths.hpp +++ b/include/graph/algorithm/common_shortest_paths.hpp @@ -158,6 +158,8 @@ class _null_range_type : public std::vector { _null_range_type(_null_range_type&& other) noexcept {} _null_range_type(_null_range_type&& other, const Allocator& alloc) {} _null_range_type(std::initializer_list init, const Allocator& alloc = Allocator()) {} + + size_t* vertex_record(_null_range_type& cont, size_t const& id) { return nullptr; } }; inline static _null_range_type _null_predecessors; diff --git a/include/graph/algorithm/dijkstra_clrs.hpp b/include/graph/algorithm/dijkstra_clrs.hpp index 71549e7..89e97c3 100644 --- a/include/graph/algorithm/dijkstra_clrs.hpp +++ b/include/graph/algorithm/dijkstra_clrs.hpp @@ -45,6 +45,8 @@ class _null_range_type : public std::vector { _null_range_type(_null_range_type&& other) noexcept {} _null_range_type(_null_range_type&& other, const Allocator& alloc) {} _null_range_type(std::initializer_list init, const Allocator& alloc = Allocator()) {} + + size_t* vertex_record(_null_range_type&, size_t const&) { return nullptr; } }; inline static _null_range_type null_predecessors; diff --git a/include/graph/algorithm/dijkstra_shortest_paths.hpp b/include/graph/algorithm/dijkstra_shortest_paths.hpp index a17df81..142eedd 100644 --- a/include/graph/algorithm/dijkstra_shortest_paths.hpp +++ b/include/graph/algorithm/dijkstra_shortest_paths.hpp @@ -21,11 +21,62 @@ #include #include + #ifndef GRAPH_DIJKSTRA_SHORTEST_PATHS_HPP # define GRAPH_DIJKSTRA_SHORTEST_PATHS_HPP namespace graph { +namespace detail { + // TODO: improve error messages by returning IDs. + + template Distances> + auto& validated_record(Distances& distances, vertex_id_t const& uid) + { + auto* rec = vertex_record(distances, uid); + if (rec == nullptr) + [[unlikely]] + throw std::out_of_range( + "dijkstra_shortest_paths: distances do not have value for a vertex id"); + return *rec; + } + + template Distances> + void validate_distances_record(G const& g, Distances& distances) + { + // TODO: change the secret `if constexpr` into another customization point + if constexpr (std::ranges::random_access_range + && std::ranges::sized_range) + { + if (size(distances) < size(vertices(g))) { + throw std::out_of_range( + std::format("dijkstra_shortest_paths: size of distances of {} is less than the number of vertices {}", + size(distances), size(vertices(g)))); + } + } + } + template Predecessor> + void validate_predecessors_record(G const& g, Predecessor& predecessor) + { + if constexpr (std::ranges::random_access_range + && std::ranges::sized_range) + { + if (size(predecessor) < size(vertices(g))) { + throw std::out_of_range( + std::format("dijkstra_shortest_paths: size of predecessor of {} is less than the number of vertices {}", + size(predecessor), size(vertices(g)))); + } + } + } + template + void validate_source(G const& g, vertex_id_t const& uid) + { + auto it = find_vertex(g, uid); + if (it == std::ranges::end(vertices(g))) + throw std::out_of_range("dijkstra_shortest_paths: source vertex id is out of range"); + } +} + /** * @brief Dijkstra's single-source shortest paths algorithm with a visitor. * @@ -56,88 +107,75 @@ namespace graph { * @tparam Compare Comparison function for Distance values. Defaults to less. * @tparam Combine Combine function for Distance values. Defaults to plus. */ -template (edge_reference_t)>, + record_for Distances, + record_for Predecessors, + class WF = function(edge_reference_t)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> + class Compare = less>, + class Combine = plus>> requires convertible_to, vertex_id_t> && // - is_arithmetic_v> && // - sized_range && // - sized_range && // - convertible_to, range_value_t> && - basic_edge_weight_function, Compare, Combine> + + convertible_to, record_t> && + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_paths( G&& g, const Sources& sources, Distances& distances, Predecessors& predecessor, - WF&& weight = [](edge_reference_t uv) { return range_value_t(1); }, // default weight(uv) -> 1 + WF&& weight = [](edge_reference_t uv) { return record_t(1); }, // default weight(uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { using id_type = vertex_id_t; - using distance_type = range_value_t; + using distance_type = record_t; using weight_type = invoke_result_t>; // relxing the target is the function of reducing the distance from the source to the target auto relax_target = [&g, &predecessor, &distances, &compare, &combine] // (edge_reference_t e, vertex_id_t uid, const weight_type& w_e) -> bool { const id_type vid = target_id(g, e); - const distance_type d_u = distances[static_cast(uid)]; - const distance_type d_v = distances[static_cast(vid)]; + const distance_type d_u = detail::validated_record(distances, uid); + const distance_type d_v = detail::validated_record(distances, vid); if (compare(combine(d_u, w_e), d_v)) { - distances[static_cast(vid)] = combine(d_u, w_e); + detail::validated_record(distances, vid) = combine(d_u, w_e); if constexpr (!is_same_v) { - predecessor[static_cast(vid)] = uid; + detail::validated_record(predecessor, vid) = uid; } return true; } return false; }; - if (size(distances) < size(vertices(g))) { - throw std::out_of_range( - std::format("dijkstra_shortest_paths: size of distances of {} is less than the number of vertices {}", - size(distances), size(vertices(g)))); - } - if constexpr (!is_same_v) { - if (size(predecessor) < size(vertices(g))) { - throw std::out_of_range( - std::format("dijkstra_shortest_paths: size of predecessor of {} is less than the number of vertices {}", - size(predecessor), size(vertices(g)))); - } - } + detail::validate_distances_record(g, distances); + if constexpr (!is_same_v) + detail::validate_predecessors_record(g, predecessor); constexpr auto zero = shortest_path_zero(); constexpr auto infinite = shortest_path_infinite_distance(); - const id_type N = static_cast(num_vertices(g)); - auto qcompare = [&distances](id_type a, id_type b) { - return distances[static_cast(a)] > distances[static_cast(b)]; + auto dist_a = detail::validated_record(distances, a); + auto dist_b = detail::validated_record(distances, b); + return dist_a > dist_b; }; using Queue = std::priority_queue, decltype(qcompare)>; Queue queue(qcompare); // (The optimizer removes this loop if on_initialize_vertex() is empty.) if constexpr (has_on_initialize_vertex) { - for (id_type uid = 0; uid < N; ++uid) { - visitor.on_initialize_vertex({uid, *find_vertex(g, uid)}); - } + auto&& vtcs = vertices(g); + for (auto it = std::ranges::begin(vtcs), end = std::ranges::end(vtcs); it != end; ++it) + visitor.on_initialize_vertex({vertex_id(g, it), *it}); } // Seed the queue with the initial vertice(s) for (auto&& source : sources) { - if (source >= N || source < 0) { - throw std::out_of_range(std::format("dijkstra_shortest_paths: source vertex id '{}' is out of range", source)); - } + detail::validate_source(g, source); queue.push(source); - distances[static_cast(source)] = zero; // mark source as discovered + detail::validated_record(distances, source) = zero; // mark source as discovered if constexpr (has_on_discover_vertex) { visitor.on_discover_vertex({source, *find_vertex(g, source)}); } @@ -165,7 +203,7 @@ constexpr void dijkstra_shortest_paths( } } - const bool is_neighbor_undiscovered = (distances[static_cast(vid)] == infinite); + const bool is_neighbor_undiscovered = (detail::validated_record(distances, vid) == infinite); const bool was_edge_relaxed = relax_target(uv, uid, w); if (is_neighbor_undiscovered) { @@ -207,18 +245,16 @@ constexpr void dijkstra_shortest_paths( } // while(!queue.empty()) } -template Distances, + record_for Predecessors, class WF = function(edge_reference_t)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires is_arithmetic_v> && // - sized_range && // - sized_range && // - convertible_to, range_value_t> && - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires + convertible_to, record_t> && + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_paths( G&& g, vertex_id_t source, @@ -226,8 +262,8 @@ constexpr void dijkstra_shortest_paths( Predecessors& predecessor, WF&& weight = [](edge_reference_t uv) { return range_value_t(1); }, // default weight(uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distances, predecessor, weight, forward(visitor), forward(compare), forward(combine)); } @@ -260,44 +296,41 @@ constexpr void dijkstra_shortest_paths( */ template Distances, class WF = function(edge_reference_t)>, class Visitor = empty_visitor, class Compare = less>, class Combine = plus>> requires convertible_to, vertex_id_t> && // - sized_range && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_distances( G&& g, const Sources& sources, Distances& distances, WF&& weight = [](edge_reference_t uv) { return range_value_t(1); }, // default weight(uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { dijkstra_shortest_paths(g, sources, distances, _null_predecessors, forward(weight), forward(visitor), forward(compare), forward(combine)); } template (edge_reference_t)>, + record_for Distances, + class WF = function(edge_reference_t)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> + class Compare = less>, + class Combine = plus>> requires is_arithmetic_v> && // - sized_range && // - basic_edge_weight_function, Compare, Combine> + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_distances( G&& g, vertex_id_t source, Distances& distances, WF&& weight = [](edge_reference_t uv) { return range_value_t(1); }, // default weight(uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distances, _null_predecessors, forward(weight), forward(visitor), forward(compare), forward(combine)); } diff --git a/include/graph/detail/graph_cpo.hpp b/include/graph/detail/graph_cpo.hpp index e56e036..0a30907 100644 --- a/include/graph/detail/graph_cpo.hpp +++ b/include/graph/detail/graph_cpo.hpp @@ -236,6 +236,7 @@ inline namespace _Cpos { inline constexpr _Vertices::_Cpo vertices; } + /** * @brief The vertex range type for a graph G. * @tparam G The graph type. @@ -440,6 +441,103 @@ using vertex_id_t = decltype(vertex_id(std::declval(), std::declval //using vertex_id_t = typename _vertex_id_type::type; + + + + + + + +//=== AK + +# if defined(__clang__) || defined(__EDG__) // TRANSITION, VSO-1681199 +# define GRAPH_DELETE_HACK = delete // Block unqualified name lookup +# else // ^^^ no workaround / workaround vvv +# define GRAPH_DELETE_HACK +# endif // ^^^ workaround ^^^ + + +namespace _Vertex_record { + void vertex_record() GRAPH_DELETE_HACK; + + template + concept _Has_ref_member = _HasClassOrEnumType<_C> && // + requires(_C&& __c, _G&& __uid) { + { _Fake_copy_init(__c.vertex_record(__uid)) }; + }; + template + concept _Has_ref_ADL = _HasClassOrEnumType<_C> // + + && requires(_C&& __c, _G&& __uid) { + { _Fake_copy_init(vertex_record(__c, __uid)) }; // intentional ADL + }; + + template + concept _Can_ref_eval = _HasClassOrEnumType<_C> && random_access_range<_C> && sized_range<_C>; + + class _Cpo { + private: + enum class _St_ref { _None, _Member, _Non_member, _Auto_eval }; + + template + [[nodiscard]] static consteval _Choice_t<_St_ref> _Choose_ref() noexcept { + static_assert(is_lvalue_reference_v<_C>); + if constexpr (_Has_ref_member<_C, _G>) { + return {_St_ref::_Member, noexcept(_Fake_copy_init(declval<_C>().vertex_record(declval<_G>())))}; + } else if constexpr (_Has_ref_ADL<_C, _G>) { + return {_St_ref::_Non_member, noexcept(_Fake_copy_init(vertex_record(declval<_C>(), declval<_G>())))}; // intentional ADL + } else if constexpr (_Can_ref_eval<_C, _G>) { + return {_St_ref::_Auto_eval, noexcept(true)}; + } else { + return {_St_ref::_None}; + } + } + + template + static constexpr _Choice_t<_St_ref> _Choice_ref = _Choose_ref<_C, _G>(); + + public: + + template + requires(_Choice_ref<_C&, _ID const&>._Strategy != _St_ref::_None) + [[nodiscard]] constexpr auto operator()(_C&& __c, _ID&& __uid) const noexcept(_Choice_ref<_C&, _ID const&>._No_throw) -> decltype(auto) { + constexpr _St_ref _Strat_ref = _Choice_ref<_C&, _ID const&>._Strategy; + + if constexpr (_Strat_ref == _St_ref::_Member) { + return __c.vertex_record(__uid); + } else if constexpr (_Strat_ref == _St_ref::_Non_member) { + //static_assert(is_reference_v); + return vertex_record(__c, __uid); // intentional ADL + } else if constexpr (_Strat_ref == _St_ref::_Auto_eval) { + if (__uid >= 0 && __uid < static_cast>(__c.size())) + return &__c[__uid]; + else + return static_cast(nullptr); + } else { + static_assert(_AlwaysFalse<_C>, "vertex_record(cont, uid) is not defined"); + } + } + }; +} // namespace _Vertex_record + +inline namespace _Cpos { + inline constexpr _Vertex_record::_Cpo vertex_record; +} + +template +using record_t = std::remove_reference_t< + decltype(*vertex_record(std::declval(), std::declval>())) +>; + +// === AK end + + + + + + + + // // find_vertex(g,uid) -> vertex_iterator_t // @@ -540,7 +638,7 @@ namespace _Edges { void edges(); # endif // ^^^ workaround ^^^ - template + template concept _Has_ref_member = requires(_G&& __g, vertex_reference_t<_G> u) { { _Fake_copy_init(u.edges(__g)) }; }; diff --git a/include/graph/graph.hpp b/include/graph/graph.hpp index 7a4c41d..8ac4cd3 100644 --- a/include/graph/graph.hpp +++ b/include/graph/graph.hpp @@ -270,6 +270,22 @@ concept sourced_index_adjacency_list = index_vertex_range && // //-------------------------------------------------------------------------------------------- +template +concept record_for = adjacency_list +&& requires(Cont& cont, vertex_id_t& id) +{ + vertex_record(cont, id); + //TODO: say that it returns a mutable reference + + // requires requires(G g) // semantic requirements + // { + // for (vertex_iterator_t it : vertices(g)) + // assert(vertex_record(cont, vertex_id(g, it)) != nullptr); + // + // }; +}; + +//---------------------------------------------------------------------------------------- # ifdef ENABLE_EDGELIST_RANGE template concept basic_edgelist_range = ranges::forward_range && negation_v>; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 97f8784..d967265 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -59,6 +59,7 @@ set(UNITTEST_SOURCES "mst_tests.cpp" "tc_tests.cpp" "cc_tests.cpp" + "map_adjacency_list_tests.cpp" "descriptor_tests.cpp" "tests.cpp" diff --git a/tests/map_adjacency_list_tests.cpp b/tests/map_adjacency_list_tests.cpp new file mode 100644 index 0000000..c32cef6 --- /dev/null +++ b/tests/map_adjacency_list_tests.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include + +#include "graph/graph.hpp" +#include "graph/views/incidence.hpp" +#include "graph/algorithm/dijkstra_shortest_paths.hpp" + + +namespace my { + +struct ID +{ + std::string value; + ID() = default; + ID(const char* s) : value(s) {} + friend auto operator<=>(ID const&, ID const&) = default; +}; + +using Graph = std::map>; + +const auto& vertices(Graph const& g) { return g; } + +ID vertex_id(Graph const& g, Graph::const_iterator it) { + return it->first; +} + +std::vector const& edges(Graph const&, const Graph::const_reference& u) { return u.second; } +std::vector const& edges(Graph const& g, ID uid) { return edges(g, *g.find(uid)); } + +ID target_id(Graph const& g, ID const& uid) { return uid; } + +auto find_vertex(Graph const& g, ID const& uid) { return g.find(uid); } + +Graph::const_reference target(Graph const& g, ID const& uv) { return *g.find(uv); } + +template +T* vertex_record(std::map& cont, my::ID id) +{ + return &cont[id]; // possibly creating an object +} + +} + + +static_assert(graph::vertex_range); +static_assert(graph::targeted_edge); +static_assert(graph::adjacency_list); + + +struct one_t +{ + double operator()(auto const&) const noexcept { return 1.0; } +}; +constexpr one_t one {}; + +struct Visit +{ + void on_initialize_vertex(my::ID const& id, graph::vertex_t const&) const {} +}; + + +namespace graph { + template + concept dijkstra_requirements = + adjacency_list + && std::ranges::input_range + && record_for + && record_for + && convertible_to, vertex_id_t> + && convertible_to, record_t> + && basic_edge_weight_function, Compare, Combine>; +} + +TEST_CASE("index-based adjacency list test", "[index][concept]") { + + using GG = std::vector>; + GG g {{1, 2}, {0, 2}, {0, 1}}; + static_assert(graph::adjacency_list); + + std::vector predecessors(3); + std::vector distances(3); + std::vector sources = {0}; + graph::dijkstra_shortest_paths(g, 0, distances, predecessors, one, Visit{}); + graph::dijkstra_shortest_paths(g, predecessors, distances, predecessors, one, Visit{}); + graph::dijkstra_shortest_distances(g, 0, distances, one, Visit{}); + graph::dijkstra_shortest_distances(g, distances, predecessors, one, Visit{}); + + static_assert(graph::dijkstra_requirements< + decltype(g), decltype(sources), decltype(distances), decltype(predecessors), one_t, + graph::empty_visitor, + std::less>, + std::plus> + >); + static_assert(graph::record_for, GG>); +} + + +TEST_CASE("lookup-based adjacency list test", "[map][concept]") { + + my::Graph g { + {"A", {"B", "C"}} + }; + + std::map predecessors; // we will store the predecessor of each vertex here + + std::map distances; + std::vector sources = {my::ID("A")}; + + + auto inc = graph::views::incidence(g, my::ID("A"), one); + graph::dijkstra_shortest_paths(g, sources, distances, predecessors, one, Visit{}); + + std::vector v; + std::vector> gg; + int id; + (void)graph::vertex_record(v, id); + + static_assert(graph::record_for, std::vector>>); + static_assert(graph::dijkstra_requirements< + decltype(g), decltype(sources), decltype(distances), decltype(predecessors), one_t, + graph::empty_visitor, + std::less>, + std::plus> + >); + + SECTION("vertices_breadth_first_search_view is an input view") { + REQUIRE(9 == 9); + } + + +} From a00ea8259f3b1013109c4b779ece713932d1ae36 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Sun, 29 Jun 2025 15:35:53 +0200 Subject: [PATCH 2/2] semantic requirements for record_for --- include/graph/graph.hpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/include/graph/graph.hpp b/include/graph/graph.hpp index 8ac4cd3..c61c583 100644 --- a/include/graph/graph.hpp +++ b/include/graph/graph.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "graph_info.hpp" #include "detail/graph_cpo.hpp" @@ -274,15 +275,15 @@ template concept record_for = adjacency_list && requires(Cont& cont, vertex_id_t& id) { - vertex_record(cont, id); - //TODO: say that it returns a mutable reference - - // requires requires(G g) // semantic requirements - // { - // for (vertex_iterator_t it : vertices(g)) - // assert(vertex_record(cont, vertex_id(g, it)) != nullptr); - // - // }; + vertex_record(cont, id); // TODO: say that it returns a mutable reference + + [](G& g) // semantic requirements + { + auto&& vtcs = vertices(g); + for (vertex_iterator_t it = std::ranges::begin(vtcs), + iend = std::ranges::end(vtcs); it != iend; ++it) + assert(vertex_record(cont, vertex_id(g, it)) != nullptr); + }; }; //----------------------------------------------------------------------------------------