From dda09a84161697519d6a435ad687295010fd642f Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Wed, 25 Mar 2026 16:54:23 -0400 Subject: [PATCH 1/2] Refactor dijkstra, bellman_ford, prim to use function-based distance/predecessor parameters - Replace container-based distances[uid]/predecessor[uid] params with function-based distance(g, uid) and predecessor(g, uid) across dijkstra_shortest_paths, dijkstra_shortest_distances, bellman_ford_shortest_paths, bellman_ford_shortest_distances, and prim (via mst.hpp) - Add new concepts to traversal_common.hpp: distance_function_for, predecessor_function_for, distance_fn_value_t type alias - Add _null_predecessor_fn / _null_predecessor for skipping predecessor tracking without a null container, and is_null_predecessor_fn_v trait - Add container_value_fn CTAD function object adaptor bridging containers to the new function API (wraps c[uid] into fn(g, uid) -> auto&) - Update all test call sites to use container_value_fn(distance) / container_value_fn(predecessor) - Remove is_arithmetic_v> from all requires clauses; basic_edge_weight_function already constrains the arithmetic relationship via assignable_from --- .../graph/adj_list/vertex_property_map.hpp | 4 +- .../algorithm/bellman_ford_shortest_paths.hpp | 151 ++++++------- .../algorithm/dijkstra_shortest_paths.hpp | 209 ++++++++---------- include/graph/algorithm/mst.hpp | 65 +++--- include/graph/algorithm/traversal_common.hpp | 66 ++++++ .../test_bellman_ford_shortest_paths.cpp | 38 ++-- .../test_dijkstra_shortest_paths.cpp | 123 ++++++----- tests/algorithms/test_mst.cpp | 53 +++-- 8 files changed, 396 insertions(+), 313 deletions(-) diff --git a/include/graph/adj_list/vertex_property_map.hpp b/include/graph/adj_list/vertex_property_map.hpp index 22b4a3d..61d68aa 100644 --- a/include/graph/adj_list/vertex_property_map.hpp +++ b/include/graph/adj_list/vertex_property_map.hpp @@ -168,11 +168,13 @@ template > struct vertex_property_map_value_impl; template +requires forward_range struct vertex_property_map_value_impl { using type = typename Container::mapped_type; }; template +requires random_access_range struct vertex_property_map_value_impl { using type = typename Container::value_type; }; @@ -204,7 +206,7 @@ using vertex_property_map_value_t = typename detail::vertex_property_map_value_i * it never iterates the map as a range. * * Satisfied by: - * - vector for index graphs (VId is integral) + * - vector / deque for index graphs (VId is integral) * - unordered_map / map for mapped graphs * * @tparam M Container type (e.g. vector, unordered_map) diff --git a/include/graph/algorithm/bellman_ford_shortest_paths.hpp b/include/graph/algorithm/bellman_ford_shortest_paths.hpp index fa7730c..9a87f79 100644 --- a/include/graph/algorithm/bellman_ford_shortest_paths.hpp +++ b/include/graph/algorithm/bellman_ford_shortest_paths.hpp @@ -212,32 +212,31 @@ void find_negative_cycle(G& g, template < adjacency_list G, input_range Sources, - class Distances, - class Predecessors, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class PredecessorFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - (is_null_range_v || vertex_property_map_for) && // - convertible_to, vertex_id_t> && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + predecessor_function_for && // + convertible_to, vertex_id_t> && // + basic_edge_weight_function, Compare, Combine> [[nodiscard]] constexpr optional> bellman_ford_shortest_paths( - G&& g, - const Sources& sources, - Distances& distances, - Predecessors& predecessor, - WF&& weight = + G&& g, + const Sources& sources, + DistanceFn&& distance, + PredecessorFn&& predecessor, + WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { using graph_type = std::remove_reference_t; using id_type = vertex_id_t; - using DistanceValue = vertex_property_map_value_t; + using DistanceValue = distance_fn_value_t; using weight_type = invoke_result_t>; using return_type = optional>; @@ -245,41 +244,24 @@ requires vertex_property_map_for && constexpr auto infinite = shortest_path_infinite_distance(); // relaxing the target is the function of reducing the distance from the source to the target - auto relax_target = [&g, &predecessor, &distances, &compare, &combine] // + auto relax_target = [&g, &predecessor, &distance, &compare, &combine] // (const edge_t& e, const vertex_id_t& uid, const weight_type& w_e) -> bool { const id_type vid = target_id(g, e); - const DistanceValue d_u = distances[uid]; + const DistanceValue d_u = distance(g, uid); if (d_u == infinite) return false; // Cannot relax via unreachable vertex (also guards against overflow) - const DistanceValue d_v = distances[vid]; + const DistanceValue d_v = distance(g, vid); if (compare(combine(d_u, w_e), d_v)) { - distances[vid] = combine(d_u, w_e); - if constexpr (!is_null_range_v) { - predecessor[vid] = uid; + distance(g, vid) = combine(d_u, w_e); + if constexpr (!is_null_predecessor_fn_v) { + predecessor(g, vid) = uid; } return true; } return false; }; - // Validate preconditions: source vertices must be valid, distances and predecessor must be sized appropriately - if constexpr (index_vertex_range) { - if (size(distances) < num_vertices(g)) { - throw std::out_of_range( - std::format("bellman_ford_shortest_paths: size of distances of {} is less than the number of vertices {}", - size(distances), num_vertices(g))); - } - - if constexpr (!is_null_range_v) { - if (size(predecessor) < num_vertices(g)) { - throw std::out_of_range(std::format( - "bellman_ford_shortest_paths: size of predecessor of {} is less than the number of vertices {}", - size(predecessor), num_vertices(g))); - } - } - } - // Seed the queue with the initial vertices for (auto&& seed_id : sources) { auto seed_it = find_vertex(g, seed_id); @@ -288,7 +270,7 @@ requires vertex_property_map_for && std::format("bellman_ford_shortest_paths: source vertex id '{}' is out of range", seed_id)); } - distances[seed_id] = zero; // mark source as discovered + distance(g, seed_id) = zero; // mark source as discovered if constexpr (has_on_discover_vertex) { visitor.on_discover_vertex(g, *seed_it); } else if constexpr (has_on_discover_vertex_id) { @@ -321,9 +303,9 @@ requires vertex_property_map_for && // Check for negative weight cycles if (at_least_one_edge_relaxed) { for (auto&& [uid, vid, uv, uv_w] : views::edgelist(g, weight)) { - if (compare(combine(distances[uid], uv_w), distances[vid])) { - if constexpr (!is_null_range_v) { - predecessor[vid] = uid; // close the cycle + if (compare(combine(distance(g, uid), uv_w), distance(g, vid))) { + if constexpr (!is_null_predecessor_fn_v) { + predecessor(g, vid) = uid; // close the cycle } if constexpr (has_on_edge_not_minimized) { visitor.on_edge_not_minimized(g, uv); @@ -353,29 +335,28 @@ requires vertex_property_map_for && */ template < adjacency_list G, - class Distances, - class Predecessors, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class PredecessorFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - (is_null_range_v || vertex_property_map_for) && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + predecessor_function_for && // + basic_edge_weight_function, Compare, Combine> [[nodiscard]] constexpr optional> bellman_ford_shortest_paths( G&& g, const vertex_id_t& source, - Distances& distances, - Predecessors& predecessor, + DistanceFn&& distance, + PredecessorFn&& predecessor, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - return bellman_ford_shortest_paths(g, subrange(&source, (&source + 1)), distances, predecessor, weight, + Compare&& compare = less>(), + Combine&& combine = plus>()) { + return bellman_ford_shortest_paths(g, subrange(&source, (&source + 1)), distance, predecessor, weight, forward(visitor), forward(compare), forward(combine)); } @@ -415,27 +396,26 @@ requires vertex_property_map_for && template < adjacency_list G, input_range Sources, - class Distances, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - convertible_to, vertex_id_t> && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + convertible_to, vertex_id_t> && // + basic_edge_weight_function, Compare, Combine> [[nodiscard]] constexpr optional> bellman_ford_shortest_distances( G&& g, const Sources& sources, - Distances& distances, + DistanceFn&& distance, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - return bellman_ford_shortest_paths(g, sources, distances, _null_predecessors, forward(weight), + Compare&& compare = less>(), + Combine&& combine = plus>()) { + return bellman_ford_shortest_paths(g, sources, distance, _null_predecessor, forward(weight), forward(visitor), forward(compare), forward(combine)); } @@ -452,26 +432,25 @@ requires vertex_property_map_for && // */ template < adjacency_list G, - class Distances, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + basic_edge_weight_function, Compare, Combine> [[nodiscard]] constexpr optional> bellman_ford_shortest_distances( G&& g, const vertex_id_t& source, - Distances& distances, + DistanceFn&& distance, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - return bellman_ford_shortest_paths(g, subrange(&source, (&source + 1)), distances, _null_predecessors, + Compare&& compare = less>(), + Combine&& combine = plus>()) { + return bellman_ford_shortest_paths(g, subrange(&source, (&source + 1)), distance, _null_predecessor, forward(weight), forward(visitor), forward(compare), forward(combine)); } diff --git a/include/graph/algorithm/dijkstra_shortest_paths.hpp b/include/graph/algorithm/dijkstra_shortest_paths.hpp index 2a395da..a737b06 100644 --- a/include/graph/algorithm/dijkstra_shortest_paths.hpp +++ b/include/graph/algorithm/dijkstra_shortest_paths.hpp @@ -44,10 +44,15 @@ using adj_list::index_vertex_range; * with non-negative edge weights. Supports custom weight functions, comparison operators, and * visitor callbacks for algorithm events. * + * Distance and predecessor are accessed through functions distance(g, uid) and predecessor(g, uid) + * respectively, enabling the values to reside on vertex properties or in external containers. + * * @tparam G The graph type. Must satisfy adjacency_list concept. * @tparam Sources Input range of source vertex IDs. - * @tparam Distances Container for storing distances (vector for index graphs, unordered_map for mapped). - * @tparam Predecessors Container for storing predecessor information. Can use _null_predecessors + * @tparam DistanceFn Function returning a mutable reference to a per-vertex distance value: + * (const G&, vertex_id_t) -> Distance&. Must return an arithmetic type. + * @tparam PredecessorFn Function returning a mutable reference to a per-vertex predecessor value: + * (const G&, vertex_id_t) -> PredecessorValue&. Can use _null_predecessor * if path reconstruction is not needed. * @tparam WF Edge weight function. Defaults to returning 1 for all edges (unweighted). * @tparam Visitor Visitor type with callbacks for algorithm events. Defaults to empty_visitor. @@ -57,48 +62,46 @@ using adj_list::index_vertex_range; * * @param g The graph to process. * @param sources Range of source vertex IDs to start from. - * @param distances [out] Shortest distances from sources. Must be sized >= num_vertices(g). - * @param predecessor [out] Predecessor information for path reconstruction. Must be sized >= num_vertices(g). - * @param weight Edge weight function: (const edge_t&) -> Distance. + * @param distance Function to access per-vertex distance: distance(g, uid) -> Distance&. + * @param predecessor Function to access per-vertex predecessor: predecessor(g, uid) -> Predecessor&. + * @param weight Edge weight function: (const G&, const edge_t&) -> Distance. * @param visitor Visitor for algorithm events (discover, examine, relax, finish). * @param compare Distance comparison function: (Distance, Distance) -> bool. * @param combine Distance combination function: (Distance, Weight) -> Distance. * - * @return void. Results are stored in the distances and predecessor output parameters. + * @return void. Results are stored via the distance and predecessor functions. * * **Mandates:** * - G must satisfy adjacency_list (index or mapped vertex containers) * - Sources must be input_range with values convertible to vertex_id_t - * - Distances must satisfy vertex_property_map_for (subscriptable by vertex_id_t) with arithmetic value type - * - Predecessors must satisfy vertex_property_map_for (or be _null_predecessors) + * - DistanceFn must satisfy distance_function_for with arithmetic return type + * - PredecessorFn must satisfy predecessor_function_for (or be _null_predecessor_fn) * - WF must satisfy basic_edge_weight_function * * **Preconditions:** * - All source vertices must be valid vertex IDs in the graph - * - For index graphs: distances.size() >= num_vertices(g) - * - For index graphs: predecessor.size() >= num_vertices(g) (unless using _null_predecessors) - * - For mapped graphs: distances and predecessors must be eagerly initialized for all vertices + * - distance(g, uid) must be valid for all vertex IDs in the graph + * - predecessor(g, uid) must be valid for all vertex IDs in the graph (unless using _null_predecessor) * - All edge weights must be non-negative * - Weight function must not throw or modify graph state * * **Effects:** - * - Modifies distances: Sets distances[v] for all vertices v - * - Modifies predecessor: Sets predecessor[v] for all reachable vertices + * - Sets distance(g, v) for all vertices v via the distance function + * - Sets predecessor(g, v) for all reachable vertices via the predecessor function * - Does not modify the graph g * * **Postconditions:** - * - distances[s] == 0 for all sources s - * - For reachable vertices v: distances[v] contains shortest distance from nearest source - * - For reachable vertices v: predecessor[v] contains predecessor in shortest path tree - * - For unreachable vertices v: distances[v] == numeric_limits::max() + * - distance(g, s) == 0 for all sources s + * - For reachable vertices v: distance(g, v) contains shortest distance from nearest source + * - For reachable vertices v: predecessor(g, v) contains predecessor in shortest path tree + * - For unreachable vertices v: distance(g, v) == numeric_limits::max() * * **Throws:** * - std::out_of_range if a source vertex ID is out of range - * - std::out_of_range if distances or predecessor are undersized * - std::out_of_range if a negative edge weight is encountered (for signed weight types) * - std::logic_error if internal invariant violation detected * - Exception guarantee: Basic. If an exception is thrown, graph g remains unchanged; - * distances and predecessor may be partially modified (indeterminate state). + * distance and predecessor values may be partially modified (indeterminate state). * * **Complexity:** * - Time: O((V + E) log V) using binary heap priority queue @@ -149,7 +152,9 @@ using adj_list::index_vertex_range; * std::vector dist(num_vertices(g), INF); * std::vector pred(num_vertices(g), 0); * - * dijkstra_shortest_paths(g, 0u, dist, pred); + * dijkstra_shortest_paths(g, 0u, + * [&dist](const auto&, auto uid) -> double& { return dist[uid]; }, + * [&pred](const auto&, auto uid) -> uint32_t& { return pred[uid]; }); * // dist == {0.0, 1.0, 3.0, 6.0} * } * ``` @@ -164,43 +169,42 @@ using adj_list::index_vertex_range; template < adjacency_list G, input_range Sources, - class Distances, - class Predecessors, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class PredecessorFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - (is_null_range_v || vertex_property_map_for) && // - convertible_to, vertex_id_t> && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + predecessor_function_for && // + convertible_to, vertex_id_t> && // + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_paths( - G&& g, - const Sources& sources, - Distances& distances, - Predecessors& predecessor, - WF&& weight = + G&& g, + const Sources& sources, + DistanceFn&& distance, + PredecessorFn&& predecessor, + WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { + Compare&& compare = less>(), + Combine&& combine = plus>()) { using graph_type = std::remove_reference_t; using id_type = vertex_id_t; - using distance_type = vertex_property_map_value_t; + using distance_type = distance_fn_value_t; using weight_type = invoke_result_t>; constexpr auto zero = shortest_path_zero(); constexpr auto infinite = shortest_path_infinite_distance(); // relaxing the target is the function of reducing the distance from the source to the target - auto relax_target = [&g, &predecessor, &distances, &compare, &combine, &weight] // + auto relax_target = [&g, &predecessor, &distance, &compare, &combine, &weight] // (const edge_t& uv, const vertex_id_t& uid) -> bool { const id_type vid = target_id(g, uv); - const distance_type d_u = distances[uid]; - const distance_type d_v = distances[vid]; + const distance_type d_u = distance(g, uid); + const distance_type d_v = distance(g, vid); const weight_type w_uv = weight(g, uv); // Negative weights are not allowed for Dijkstra's algorithm @@ -212,31 +216,15 @@ constexpr void dijkstra_shortest_paths( } if (compare(combine(d_u, w_uv), d_v)) { - distances[vid] = combine(d_u, w_uv); - if constexpr (!is_null_range_v) { - predecessor[vid] = uid; + distance(g, vid) = combine(d_u, w_uv); + if constexpr (!is_null_predecessor_fn_v) { + predecessor(g, vid) = uid; } return true; } return false; }; - // Validate preconditions: source vertices must be valid, distances and predecessor must be sized appropriately - if constexpr (index_vertex_range) { - if (size(distances) < num_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), num_vertices(g))); - } - if constexpr (!is_null_range_v) { - if (size(predecessor) < num_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), num_vertices(g))); - } - } - } - // Define and initialize the priority queue for Dijkstra's algorithm. We use a min-heap based on distance. struct weighted_vertex { vertex_t vertex_desc = {}; @@ -267,7 +255,7 @@ constexpr void dijkstra_shortest_paths( } vertex_t seed = *seed_it; - distances[seed_id] = zero; // mark seed_id as discovered + distance(g, seed_id) = zero; // mark seed_id as discovered queue.push({seed, zero}); if constexpr (has_on_discover_vertex) { visitor.on_discover_vertex(g, seed); @@ -293,7 +281,7 @@ constexpr void dijkstra_shortest_paths( visitor.on_examine_edge(g, uv); } - const bool is_neighbor_undiscovered = (distances[vid] == infinite); + const bool is_neighbor_undiscovered = (distance(g, vid) == infinite); const bool was_edge_relaxed = relax_target(uv, uid); if (is_neighbor_undiscovered) { @@ -308,7 +296,7 @@ constexpr void dijkstra_shortest_paths( } else if constexpr (has_on_discover_vertex_id) { visitor.on_discover_vertex(g, vid); } - queue.push({v, distances[vid]}); + queue.push({v, distance(g, vid)}); } else { // This is an indicator of a bug in the algorithm and should be investigated. throw std::logic_error( @@ -321,7 +309,7 @@ constexpr void dijkstra_shortest_paths( if constexpr (has_on_edge_relaxed) { visitor.on_edge_relaxed(g, uv); } - queue.push({v, distances[vid]}); // re-enqueue with updated distance + queue.push({v, distance(g, vid)}); // re-enqueue with updated distance } else { if constexpr (has_on_edge_not_relaxed) { visitor.on_edge_not_relaxed(g, uv); @@ -348,33 +336,32 @@ constexpr void dijkstra_shortest_paths( * * @param source Single source vertex ID instead of range. * - * @see dijkstra_shortest_paths(G&&, const Sources&, Distances&, Predecessors&, WF&&, Visitor&&, Compare&&, Combine&&) + * @see dijkstra_shortest_paths (multi-source overload) */ template < adjacency_list G, - class Distances, - class Predecessors, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class PredecessorFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - (is_null_range_v || vertex_property_map_for) && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + predecessor_function_for && // + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_paths( G&& g, const vertex_id_t& source, - Distances& distances, - Predecessors& predecessor, + DistanceFn&& distance, + PredecessorFn&& predecessor, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distances, predecessor, weight, + Compare&& compare = less>(), + Combine&& combine = plus>()) { + dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distance, predecessor, weight, forward(visitor), forward(compare), forward(combine)); } @@ -386,7 +373,7 @@ constexpr void dijkstra_shortest_paths( * * @tparam G The graph type. Must satisfy adjacency_list concept. * @tparam Sources Input range of source vertex IDs. - * @tparam Distances Container for storing distances. Value type must be arithmetic. + * @tparam DistanceFn Function returning a mutable reference to a per-vertex distance value. * @tparam WF Edge weight function. Defaults to returning 1 for all edges (unweighted). * @tparam Visitor Visitor type with callbacks for algorithm events. Defaults to empty_visitor. * @tparam Compare Comparison function for distance values. Defaults to less<>. @@ -394,45 +381,44 @@ constexpr void dijkstra_shortest_paths( * * @param g The graph to process. * @param sources Range of source vertex IDs to start from. - * @param distances [out] Shortest distances from sources. Must be sized >= num_vertices(g). - * @param weight Edge weight function: (const edge_t&) -> Distance. + * @param distance Function to access per-vertex distance: distance(g, uid) -> Distance&. + * @param weight Edge weight function: (const G&, const edge_t&) -> Distance. * @param visitor Visitor for algorithm events (discover, examine, relax, finish). * @param compare Distance comparison function: (Distance, Distance) -> bool. * @param combine Distance combination function: (Distance, Weight) -> Distance. * - * @return void. Results are stored in the distances output parameter. + * @return void. Results are stored via the distance function. * * **Effects:** - * - Modifies distances: Sets distances[v] for all vertices v + * - Sets distance(g, v) for all vertices v via the distance function * - Does not modify the graph g - * - Internally uses _null_predecessors to skip predecessor tracking + * - Internally uses _null_predecessor to skip predecessor tracking * * @see dijkstra_shortest_paths() for full documentation and complexity analysis. */ template < adjacency_list G, input_range Sources, - class Distances, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - convertible_to, vertex_id_t> && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + convertible_to, vertex_id_t> && // + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_distances( G&& g, const Sources& sources, - Distances& distances, + DistanceFn&& distance, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - dijkstra_shortest_paths(g, sources, distances, _null_predecessors, forward(weight), forward(visitor), + Compare&& compare = less>(), + Combine&& combine = plus>()) { + dijkstra_shortest_paths(g, sources, distance, _null_predecessor, forward(weight), forward(visitor), forward(compare), forward(combine)); } @@ -447,26 +433,25 @@ constexpr void dijkstra_shortest_distances( */ template < adjacency_list G, - class Distances, - class WF = function(const std::remove_reference_t&, const edge_t&)>, + class DistanceFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, class Visitor = empty_visitor, - class Compare = less>, - class Combine = plus>> -requires vertex_property_map_for && // - is_arithmetic_v> && // - basic_edge_weight_function, Compare, Combine> + class Compare = less>, + class Combine = plus>> +requires distance_function_for && // + basic_edge_weight_function, Compare, Combine> constexpr void dijkstra_shortest_distances( G&& g, const vertex_id_t& source, - Distances& distances, + DistanceFn&& distance, WF&& weight = [](const auto&, const edge_t& uv) { - return vertex_property_map_value_t(1); + return distance_fn_value_t(1); }, // default weight(g, uv) -> 1 Visitor&& visitor = empty_visitor(), - Compare&& compare = less>(), - Combine&& combine = plus>()) { - dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distances, _null_predecessors, forward(weight), + Compare&& compare = less>(), + Combine&& combine = plus>()) { + dijkstra_shortest_paths(g, subrange(&source, (&source + 1)), distance, _null_predecessor, forward(weight), forward(visitor), forward(compare), forward(combine)); } diff --git a/include/graph/algorithm/mst.hpp b/include/graph/algorithm/mst.hpp index 0e82ce6..1d0f2ae 100644 --- a/include/graph/algorithm/mst.hpp +++ b/include/graph/algorithm/mst.hpp @@ -802,16 +802,16 @@ auto inplace_kruskal(IELR&& e, // graph * edge connecting a tree vertex to a non-tree vertex. Delegates to dijkstra_shortest_paths * with a projecting combine function. * - * @tparam G Graph type satisfying adjacency_list. - * @tparam Predecessor Vertex property map for predecessor output. - * @tparam Weight Vertex property map for edge weight output. - * @tparam WF Edge weight function type. Defaults to edge_value(g, uv). - * @tparam CompareOp Comparison operator type. Defaults to less<>. + * @tparam G Graph type satisfying adjacency_list. + * @tparam PredecessorFn Function type returning lvalue ref to predecessor for a vertex. + * @tparam WeightFn Function type returning lvalue ref to edge weight for a vertex. + * @tparam WF Edge weight function type. Defaults to edge_value(g, uv). + * @tparam CompareOp Comparison operator type. Defaults to less<>. * * @param g The graph to process. * @param seed Starting vertex for MST growth. - * @param predecessor Output: predecessor[v] = parent of v in MST, predecessor[seed] = seed. - * @param weight Output: weight[v] = edge weight from predecessor[v] to v. + * @param weight Function weight(g, uid) -> edge_weight&: returns lvalue ref to edge weight for vertex uid. + * @param predecessor Function predecessor(g, uid) -> vertex_id&: returns lvalue ref to predecessor for vertex uid. * @param weight_fn Edge weight function (default: edge_value). * @param compare Comparison for edge weights (default: less<>). * @@ -819,25 +819,24 @@ auto inplace_kruskal(IELR&& e, // graph * * **Mandates:** * - G must satisfy adjacency_list - * - Predecessor must satisfy vertex_property_map_for - * - Weight must satisfy vertex_property_map_for + * - WeightFn must satisfy distance_function_for + * - PredecessorFn must satisfy predecessor_function_for * - WF must satisfy basic_edge_weight_function * * **Preconditions:** * - seed must be a valid vertex in the graph * - predecessor and weight must be initialized (use init_shortest_paths) - * - For index graphs: predecessor.size() >= num_vertices(g) and weight.size() >= num_vertices(g) * - compare must define a strict weak ordering * * **Effects:** - * - Writes MST structure to predecessor and weight arrays + * - Writes MST structure via predecessor and weight functions * - Does not modify graph g * * **Postconditions:** - * - predecessor[seed] == seed - * - For reachable vertices: predecessor[v] points to parent in MST - * - For unreachable vertices: predecessor[v] is unchanged - * - weight[v] contains edge weight from predecessor[v] to v + * - predecessor(g, seed) == seed + * - For reachable vertices: predecessor(g, v) points to parent in MST + * - For unreachable vertices: predecessor(g, v) is unchanged + * - weight(g, v) contains edge weight from predecessor(g, v) to v * * **Returns:** * - Total weight of the spanning tree (edge_value_type) @@ -845,7 +844,6 @@ auto inplace_kruskal(IELR&& e, // graph * * **Throws:** * - std::out_of_range if seed vertex ID is out of range - * - std::out_of_range if predecessor or weight are undersized * - Exception guarantee: Basic. Graph g unchanged; predecessor/weight may be partial. * * **Complexity:** @@ -862,43 +860,48 @@ auto inplace_kruskal(IELR&& e, // graph * std::vector pred(num_vertices(g)); * std::vector wt(num_vertices(g)); * init_shortest_paths(g, wt, pred); - * auto total_weight = prim(g, 0, pred, wt); + * auto total_weight = prim(g, 0, + * [&wt](const auto& g, auto uid) -> auto& { return wt[uid]; }, + * [&pred](const auto& g, auto uid) -> auto& { return pred[uid]; }); * ``` */ template (const std::remove_reference_t&, const edge_t&)>, - class CompareOp = less>> -requires vertex_property_map_for && - vertex_property_map_for && - basic_edge_weight_function, CompareOp, plus>> + class WeightFn, + class PredecessorFn, + class WF = function(const std::remove_reference_t&, const edge_t&)>, + class CompareOp = less>> +requires distance_function_for && + is_arithmetic_v> && + predecessor_function_for && + basic_edge_weight_function, CompareOp, plus>> auto prim(G&& g, // graph const vertex_id_t& seed, // seed vtx - Predecessor& predecessor, // out: predecessor[uid] of uid in tree - Weight& weight, // out: edge value weight[uid] from tree edge uid to predecessor[uid] + WeightFn&& weight, // out: weight(g, uid) -> edge weight& from tree edge uid to predecessor + PredecessorFn&& predecessor, // out: predecessor(g, uid) -> predecessor& of uid in tree WF&& weight_fn = [](const auto& gr, const edge_t& uv) { return edge_value(gr, uv); }, // default weight_fn(g, uv) -> edge_value(g, uv) - CompareOp compare = less>() // edge value comparator + CompareOp compare = less>() // edge value comparator ) { - using edge_value_type = vertex_property_map_value_t; + using edge_value_type = distance_fn_value_t; // Prim's combine: ignore accumulated distance, use edge weight directly. // This transforms Dijkstra's relaxation check from compare(d_u + w, d_v) // to compare(w, d_v), which is exactly Prim's criterion. auto prim_combine = [](edge_value_type /*d_u*/, edge_value_type w_uv) -> edge_value_type { return w_uv; }; - dijkstra_shortest_paths(g, seed, weight, predecessor, + dijkstra_shortest_paths(g, seed, + std::forward(weight), + std::forward(predecessor), std::forward(weight_fn), empty_visitor(), std::forward(compare), prim_combine); // Calculate total MST weight by summing edge weights edge_value_type total_weight = edge_value_type{}; for (auto [vid] : views::basic_vertexlist(g)) { - if (vid != seed && predecessor[vid] != vid) { - total_weight += weight[vid]; + if (vid != seed && predecessor(g, vid) != vid) { + total_weight += weight(g, vid); } } diff --git a/include/graph/algorithm/traversal_common.hpp b/include/graph/algorithm/traversal_common.hpp index 3e5ed53..4a219b9 100644 --- a/include/graph/algorithm/traversal_common.hpp +++ b/include/graph/algorithm/traversal_common.hpp @@ -135,6 +135,72 @@ constexpr auto shortest_path_zero() { return DistanceValue(); } +// +// Distance and predecessor function concepts +// +// These concepts enable algorithms to accept functions for per-vertex distance and +// predecessor access: distance(g, uid) and predecessor(g, uid). This is more flexible +// than requiring a specific container, because the values can reside on a vertex property +// or in an external container. +// + +/// Type alias: extracts the distance value type from a distance function's return type +template +using distance_fn_value_t = std::remove_cvref_t< + std::invoke_result_t&, const vertex_id_t&>>; + +/// Concept: a callable returning a mutable reference to a per-vertex distance value +template +concept distance_function_for = + std::invocable&, const vertex_id_t&> && + std::is_lvalue_reference_v< + std::invoke_result_t&, const vertex_id_t&>>; + +/// Concept: a callable returning a mutable reference to a per-vertex predecessor value +template +concept predecessor_function_for = + std::invocable&, const vertex_id_t&> && + std::is_lvalue_reference_v< + std::invoke_result_t&, const vertex_id_t&>>; + +/// Null predecessor function — used when predecessor tracking is not needed. +/// Detected at compile time via is_null_predecessor_fn_v to skip predecessor writes. +struct _null_predecessor_fn { + template + size_t& operator()(const G&, const VId&) { + static size_t dummy = 0; + return dummy; + } +}; + +/// Global instance of the null predecessor function +inline _null_predecessor_fn _null_predecessor; + +/// Type trait to detect _null_predecessor_fn at compile time +template +inline constexpr bool is_null_predecessor_fn_v = std::is_same_v, _null_predecessor_fn>; + +/// Function object that adapts a subscriptable container into a property function. +/// Wraps container[uid] into fn(g, uid) -> auto&, satisfying distance_function_for +/// and predecessor_function_for concepts. +/// +/// Usage: +/// std::vector distances(num_vertices(g)); +/// dijkstra_shortest_paths(g, source, container_value_fn(distances), ...); +/// +template +struct container_value_fn { + Container& c; + + template + constexpr auto& operator()(const G&, const VId& uid) const { + return c[uid]; + } +}; + +template +container_value_fn(Container&) -> container_value_fn; + // // Visitor concepts // diff --git a/tests/algorithms/test_bellman_ford_shortest_paths.cpp b/tests/algorithms/test_bellman_ford_shortest_paths.cpp index 6df7521..78461d7 100644 --- a/tests/algorithms/test_bellman_ford_shortest_paths.cpp +++ b/tests/algorithms/test_bellman_ford_shortest_paths.cpp @@ -55,7 +55,7 @@ TEST_CASE("bellman_ford_shortest_paths - CLRS example", "[algorithm][bellman_for init_shortest_paths(g, distance, predecessor); - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor, + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle should be detected @@ -78,7 +78,7 @@ TEST_CASE("bellman_ford_shortest_paths - path graph", "[algorithm][bellman_ford_ init_shortest_paths(g, distance, predecessor); - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor, + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -99,7 +99,7 @@ TEST_CASE("bellman_ford_shortest_distances - no predecessors", "[algorithm][bell init_shortest_paths(g, distance); // Test distances-only variant (no predecessor tracking) - auto result = bellman_ford_shortest_distances(g, vertex_id_t(0), distance, + auto result = bellman_ford_shortest_distances(g, vertex_id_t(0), container_value_fn(distance), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -125,7 +125,7 @@ TEST_CASE("bellman_ford_shortest_paths - multi-source", "[algorithm][bellman_for // Start from vertices 0 and 3 std::vector> sources = {0, 3}; - auto result = bellman_ford_shortest_paths(g, sources, distance, predecessor, + auto result = bellman_ford_shortest_paths(g, sources, container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -151,7 +151,7 @@ TEST_CASE("bellman_ford_shortest_distances - multi-source", "[algorithm][bellman // Start from vertices 0 and 3 std::vector> sources = {0, 3}; - auto result = bellman_ford_shortest_distances(g, sources, distance, + auto result = bellman_ford_shortest_distances(g, sources, container_value_fn(distance), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -174,7 +174,7 @@ TEST_CASE("bellman_ford_shortest_paths - with visitor", "[algorithm][bellman_for BellmanCountingVisitor visitor; auto result = bellman_ford_shortest_paths( - g, vertex_id_t(0), distance, predecessor, + g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); // No negative cycle @@ -201,7 +201,7 @@ TEST_CASE("bellman_ford_shortest_paths - unweighted graph (default weight)", init_shortest_paths(g, distance, predecessor); // Use default weight function (returns 1 for all edges) - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor); + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor)); // No negative cycle REQUIRE(!result.has_value()); @@ -221,7 +221,7 @@ TEST_CASE("bellman_ford_shortest_paths - predecessor path reconstruction", "[alg init_shortest_paths(g, distance, predecessor); - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor, + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -260,7 +260,7 @@ TEST_CASE("bellman_ford_shortest_paths - unreachable vertices", "[algorithm][bel init_shortest_paths(g, distance, predecessor); - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor); + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor)); // No negative cycle REQUIRE(!result.has_value()); @@ -292,7 +292,7 @@ TEST_CASE("bellman_ford_shortest_paths - negative weight cycle detection", "[alg BellmanCountingVisitor visitor; auto result = bellman_ford_shortest_paths( - g, vertex_id_t(0), distance, predecessor, + g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); // Negative cycle should be detected @@ -316,7 +316,7 @@ TEST_CASE("bellman_ford_shortest_paths - find negative cycle vertices", "[algori init_shortest_paths(g, distance, predecessor); - auto cycle_vertex = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor, + auto cycle_vertex = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); REQUIRE(cycle_vertex.has_value()); @@ -353,7 +353,7 @@ TEST_CASE("bellman_ford_shortest_paths - single vertex", "[algorithm][bellman_fo init_shortest_paths(g, distance, predecessor); - auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), distance, predecessor); + auto result = bellman_ford_shortest_paths(g, vertex_id_t(0), container_value_fn(distance), container_value_fn(predecessor)); // No negative cycle REQUIRE(!result.has_value()); @@ -385,7 +385,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse CLRS example", for (auto&& [uid, u] : views::vertexlist(g)) predecessors[uid] = uid; - auto result = bellman_ford_shortest_paths(g, id_type(exp.s), distances, predecessors, + auto result = bellman_ford_shortest_paths(g, id_type(exp.s), container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle should be detected @@ -410,7 +410,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_distances - sparse CLRS example", auto g = map_fixtures::clrs_dijkstra_graph(); auto distances = make_vertex_property_map(g, shortest_path_infinite_distance()); - auto result = bellman_ford_shortest_distances(g, id_type(exp.s), distances, + auto result = bellman_ford_shortest_distances(g, id_type(exp.s), container_value_fn(distances), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -436,7 +436,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse multi-source", // Start from s (10) and y (40) std::vector sources = {exp.s, exp.y}; - auto result = bellman_ford_shortest_paths(g, sources, distances, predecessors, + auto result = bellman_ford_shortest_paths(g, sources, container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // No negative cycle @@ -467,7 +467,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse with visitor", BellmanCountingVisitor visitor; auto result = bellman_ford_shortest_paths( - g, id_type(exp.s), distances, predecessors, + g, id_type(exp.s), container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); REQUIRE(!result.has_value()); @@ -491,7 +491,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse negative cycle detectio predecessors[uid] = uid; auto result = bellman_ford_shortest_paths( - g, id_type(10), distances, predecessors, + g, id_type(10), container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Negative cycle should be detected @@ -516,7 +516,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse find negative cycle ver predecessors[uid] = uid; auto cycle_vertex = bellman_ford_shortest_paths( - g, id_type(10), distances, predecessors, + g, id_type(10), container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); REQUIRE(cycle_vertex.has_value()); @@ -546,7 +546,7 @@ TEMPLATE_TEST_CASE("bellman_ford_shortest_paths - sparse source not in graph thr predecessors[uid] = uid; // Vertex ID 999 does not exist in the sparse graph - CHECK_THROWS_AS(bellman_ford_shortest_paths(g, id_type(999), distances, predecessors, + CHECK_THROWS_AS(bellman_ford_shortest_paths(g, id_type(999), container_value_fn(distances), container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }), std::out_of_range); } diff --git a/tests/algorithms/test_dijkstra_shortest_paths.cpp b/tests/algorithms/test_dijkstra_shortest_paths.cpp index 8be9ca2..136cdcf 100644 --- a/tests/algorithms/test_dijkstra_shortest_paths.cpp +++ b/tests/algorithms/test_dijkstra_shortest_paths.cpp @@ -49,7 +49,9 @@ TEST_CASE("dijkstra_shortest_paths - CLRS example", "[algorithm][dijkstra_shorte init_shortest_paths(g, distance, predecessor); - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor, + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Validate against known results from CLRS Figure 24.6 @@ -69,7 +71,9 @@ TEST_CASE("dijkstra_shortest_paths - path graph", "[algorithm][dijkstra_shortest init_shortest_paths(g, distance, predecessor); - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor, + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Path: 0 -> 1 -> 2 -> 3 with weight 1 each @@ -87,7 +91,8 @@ TEST_CASE("dijkstra_shortest_distances - no predecessors", "[algorithm][dijkstra init_shortest_paths(g, distance); // Test distances-only variant (no predecessor tracking) - dijkstra_shortest_distances(g, vertex_id_t(0), distance, + dijkstra_shortest_distances(g, vertex_id_t(0), + container_value_fn(distance), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Validate distances match expected results @@ -110,7 +115,9 @@ TEST_CASE("dijkstra_shortest_paths - multi-source", "[algorithm][dijkstra_shorte // Start from vertices 0 and 3 std::vector> sources = {0, 3}; - dijkstra_shortest_paths(g, sources, distance, predecessor, + dijkstra_shortest_paths(g, sources, + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Both source vertices should have distance 0 @@ -133,7 +140,9 @@ TEST_CASE("dijkstra_shortest_distances - multi-source", "[algorithm][dijkstra_sh // Start from vertices 0 and 3 std::vector> sources = {0, 3}; - dijkstra_shortest_distances(g, sources, distance, [](const auto& g, const auto& uv) { return edge_value(g, uv); }); + dijkstra_shortest_distances(g, sources, + container_value_fn(distance), + [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Both source vertices should have distance 0 REQUIRE(distance[0] == 0); @@ -152,7 +161,9 @@ TEST_CASE("dijkstra_shortest_paths - with visitor", "[algorithm][dijkstra_shorte CountingVisitor visitor; dijkstra_shortest_paths( - g, vertex_id_t(0), distance, predecessor, + g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); // Verify visitor was called (should have discovered all 4 vertices, examined them, and relaxed edges) @@ -176,7 +187,9 @@ TEST_CASE("dijkstra_shortest_paths - unweighted graph (default weight)", "[algor init_shortest_paths(g, distance, predecessor); // Use default weight function (returns 1 for all edges) - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor); + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor)); REQUIRE(distance[0] == 0); REQUIRE(distance[1] == 1); @@ -193,7 +206,9 @@ TEST_CASE("dijkstra_shortest_paths - predecessor path reconstruction", "[algorit init_shortest_paths(g, distance, predecessor); - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor, + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Reconstruct path from 0 to 3: should be 0 -> 1 -> 2 -> 3 @@ -228,7 +243,9 @@ TEST_CASE("dijkstra_shortest_paths - unreachable vertices", "[algorithm][dijkstr init_shortest_paths(g, distance, predecessor); - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor); + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor)); // Vertices 0 and 1 should be reachable REQUIRE(distance[0] == 0); @@ -270,7 +287,9 @@ TEST_CASE("dijkstra_shortest_paths - vertex id visitor", "[algorithm][dijkstra_s init_shortest_paths(g, distance, predecessor); IdCountingVisitor visitor; - dijkstra_shortest_paths(g, vertex_id_t(0), distance, predecessor, + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distance), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); // All 5 vertices should be discovered via id-based callbacks @@ -287,32 +306,6 @@ TEST_CASE("dijkstra_shortest_paths - vertex id visitor", "[algorithm][dijkstra_s // Error / Exception Condition Tests // ============================================================================= -TEST_CASE("dijkstra_shortest_paths - distances too small throws", "[algorithm][dijkstra_shortest_paths][error]") { - using Graph = vov_weighted; - - auto g = clrs_dijkstra_graph(); // 5 vertices - std::vector distances(2); // too small - std::vector> predecessor(num_vertices(g)); - init_shortest_paths(g, distances, predecessor); - - CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, - [](const auto& g, const auto& uv) { return edge_value(g, uv); }), - std::out_of_range); -} - -TEST_CASE("dijkstra_shortest_paths - predecessor too small throws", "[algorithm][dijkstra_shortest_paths][error]") { - using Graph = vov_weighted; - - auto g = clrs_dijkstra_graph(); // 5 vertices - std::vector distances(num_vertices(g)); - std::vector> predecessor(2); // too small - init_shortest_paths(g, distances, predecessor); - - CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, - [](const auto& g, const auto& uv) { return edge_value(g, uv); }), - std::out_of_range); -} - TEST_CASE("dijkstra_shortest_paths - source vertex out of range throws", "[algorithm][dijkstra_shortest_paths][error]") { using Graph = vov_weighted; @@ -321,7 +314,9 @@ TEST_CASE("dijkstra_shortest_paths - source vertex out of range throws", "[algor std::vector> predecessor(num_vertices(g)); init_shortest_paths(g, distances, predecessor); - CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(99), distances, predecessor, + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(99), + container_value_fn(distances), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }), std::out_of_range); } @@ -335,7 +330,9 @@ TEST_CASE("dijkstra_shortest_paths - negative edge weight throws", "[algorithm][ init_shortest_paths(g, distances, predecessor); // Weight function that always returns a negative value triggers the signed-weight guard - CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distances), + container_value_fn(predecessor), [](const auto&, const auto&) { return -1; }), std::out_of_range); } @@ -353,7 +350,9 @@ TEST_CASE("dijkstra_shortest_paths - infinite weight edge triggers logic_error", init_shortest_paths(g, distances, predecessor); const auto INF = shortest_path_infinite_distance(); - CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + CHECK_THROWS_AS(dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distances), + container_value_fn(predecessor), [INF](const auto&, const auto&) { return INF; }), std::logic_error); } @@ -369,7 +368,9 @@ TEST_CASE("dijkstra_shortest_paths - on_edge_not_relaxed visitor callback", "[al init_shortest_paths(g, distances, predecessor); CountingVisitor visitor; - dijkstra_shortest_paths(g, vertex_id_t(0), distances, predecessor, + dijkstra_shortest_paths(g, vertex_id_t(0), + container_value_fn(distances), + container_value_fn(predecessor), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); CHECK(visitor.edges_not_relaxed > 0); @@ -403,7 +404,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - sparse CLRS example", for (auto&& [uid, u] : views::vertexlist(g)) predecessors[uid] = uid; - dijkstra_shortest_paths(g, id_type(exp.s), distances, predecessors, + dijkstra_shortest_paths(g, id_type(exp.s), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Validate distances against known CLRS results @@ -425,7 +428,8 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_distances - sparse CLRS example", auto g = map_fixtures::clrs_dijkstra_graph(); auto distances = make_vertex_property_map(g, shortest_path_infinite_distance()); - dijkstra_shortest_distances(g, id_type(exp.s), distances, + dijkstra_shortest_distances(g, id_type(exp.s), + container_value_fn(distances), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); for (size_t i = 0; i < exp.num_vertices; ++i) { @@ -448,7 +452,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - sparse multi-source", // Start from s (10) and y (40) std::vector sources = {exp.s, exp.y}; - dijkstra_shortest_paths(g, sources, distances, predecessors, + dijkstra_shortest_paths(g, sources, + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Both sources should have distance 0 @@ -476,7 +482,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - sparse with visitor", CountingVisitor visitor; dijkstra_shortest_paths( - g, id_type(exp.s), distances, predecessors, + g, id_type(exp.s), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); REQUIRE(visitor.vertices_discovered == static_cast(exp.num_vertices)); @@ -497,7 +505,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - sparse source not in graph throws" predecessors[uid] = uid; // Vertex ID 999 does not exist in the sparse graph - CHECK_THROWS_AS(dijkstra_shortest_paths(g, id_type(999), distances, predecessors, + CHECK_THROWS_AS(dijkstra_shortest_paths(g, id_type(999), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }), std::out_of_range); } @@ -563,7 +573,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - string vertex IDs", for (auto&& [uid, u] : views::vertexlist(g)) predecessors[uid] = uid; - dijkstra_shortest_paths(g, std::string("s"), distances, predecessors, + dijkstra_shortest_paths(g, std::string("s"), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Validate all distances @@ -587,7 +599,8 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_distances - string vertex IDs", auto g = clrs_dijkstra_string_graph(); auto distances = make_vertex_property_map(g, shortest_path_infinite_distance()); - dijkstra_shortest_distances(g, std::string("s"), distances, + dijkstra_shortest_distances(g, std::string("s"), + container_value_fn(distances), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); for (const auto& [vid, expected_dist] : clrs_string_expected::distances) { @@ -609,7 +622,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - string vertex IDs multi-source", // Start from "s" and "y" std::vector sources = {std::string("s"), std::string("y")}; - dijkstra_shortest_paths(g, sources, distances, predecessors, + dijkstra_shortest_paths(g, sources, + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Both sources should have distance 0 @@ -636,7 +651,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - string vertex IDs with visitor", CountingVisitor visitor; dijkstra_shortest_paths( - g, std::string("s"), distances, predecessors, + g, std::string("s"), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, visitor); REQUIRE(visitor.vertices_discovered == static_cast(clrs_string_expected::num_vertices)); @@ -658,7 +675,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - string vertex IDs invalid source t predecessors[uid] = uid; // "nonexistent" is not a vertex in the graph - CHECK_THROWS_AS(dijkstra_shortest_paths(g, std::string("nonexistent"), distances, predecessors, + CHECK_THROWS_AS(dijkstra_shortest_paths(g, std::string("nonexistent"), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }), std::out_of_range); } @@ -675,7 +694,9 @@ TEMPLATE_TEST_CASE("dijkstra_shortest_paths - string vertex IDs path reconstruct for (auto&& [uid, u] : views::vertexlist(g)) predecessors[uid] = uid; - dijkstra_shortest_paths(g, std::string("s"), distances, predecessors, + dijkstra_shortest_paths(g, std::string("s"), + container_value_fn(distances), + container_value_fn(predecessors), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // Reconstruct path from "s" to "x": should be s -> y -> t -> x (cost 5+3+1=9) diff --git a/tests/algorithms/test_mst.cpp b/tests/algorithms/test_mst.cpp index c023f38..8589c02 100644 --- a/tests/algorithms/test_mst.cpp +++ b/tests/algorithms/test_mst.cpp @@ -309,7 +309,9 @@ TEST_CASE("prim - simple triangle", "[algorithm][mst][prim]") { std::vector weight(3); init_shortest_paths(g, weight, predecessor); - auto total_wt = prim(g, 0, predecessor, weight); + auto total_wt = prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); // Check MST properties REQUIRE(predecessor[0] == 0); // Root @@ -330,7 +332,9 @@ TEST_CASE("prim - linear graph", "[algorithm][mst][prim]") { std::vector weight(4); init_shortest_paths(g, weight, predecessor); - prim(g, 0, predecessor, weight); + prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); REQUIRE(predecessor[0] == 0); // Root @@ -360,7 +364,9 @@ TEST_CASE("prim - complete graph K4", "[algorithm][mst][prim]") { std::vector weight(4); init_shortest_paths(g, weight, predecessor); - prim(g, 0, predecessor, weight); + prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); REQUIRE(predecessor[0] == 0); @@ -400,7 +406,9 @@ TEST_CASE("kruskal and prim produce same MST weight", "[algorithm][mst]") { std::vector predecessor(5); std::vector weight(5); init_shortest_paths(g, weight, predecessor); - prim(g, 0, predecessor, weight); + prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); int prim_weight = weight[1] + weight[2] + weight[3] + weight[4]; @@ -424,7 +432,9 @@ TEST_CASE("prim - undirected_adjacency_list triangle", "[algorithm][mst][prim][u std::vector weight(3); init_shortest_paths(g, weight, predecessor); - auto total_wt = prim(g, 0, predecessor, weight); + auto total_wt = prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); // Check MST properties REQUIRE(predecessor[0] == 0); // Root @@ -446,7 +456,9 @@ TEST_CASE("prim - undirected_adjacency_list linear graph", "[algorithm][mst][pri std::vector weight(4); init_shortest_paths(g, weight, predecessor); - auto total_wt = prim(g, 0, predecessor, weight); + auto total_wt = prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); REQUIRE(predecessor[0] == 0); // Root @@ -466,7 +478,9 @@ TEST_CASE("prim - undirected_adjacency_list complete graph K4", "[algorithm][mst std::vector weight(4); init_shortest_paths(g, weight, predecessor); - auto total_wt = prim(g, 0, predecessor, weight); + auto total_wt = prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); REQUIRE(predecessor[0] == 0); @@ -486,7 +500,9 @@ TEST_CASE("prim - undirected_adjacency_list CLRS example", "[algorithm][mst][pri std::vector weight(5); init_shortest_paths(g, weight, predecessor); - auto total_wt = prim(g, 0, predecessor, weight); + auto total_wt = prim(g, 0, + container_value_fn(weight), + container_value_fn(predecessor)); REQUIRE(predecessor[0] == 0); // Root @@ -520,7 +536,9 @@ TEMPLATE_TEST_CASE("prim - sparse triangle", auto weight_map = make_vertex_property_map(g, 0); init_shortest_paths(g, weight_map, predecessor); - auto total_wt = prim(g, id_type(10), predecessor, weight_map); + auto total_wt = prim(g, id_type(10), + container_value_fn(weight_map), + container_value_fn(predecessor)); REQUIRE(predecessor[id_type(10)] == id_type(10)); // Root REQUIRE(total_wt == 3); // 1 + 2 @@ -539,7 +557,9 @@ TEMPLATE_TEST_CASE("prim - sparse linear graph", auto weight_map = make_vertex_property_map(g, 0); init_shortest_paths(g, weight_map, predecessor); - auto total_wt = prim(g, id_type(10), predecessor, weight_map); + auto total_wt = prim(g, id_type(10), + container_value_fn(weight_map), + container_value_fn(predecessor)); REQUIRE(predecessor[id_type(10)] == id_type(10)); // Root REQUIRE(total_wt == 6); // 1 + 2 + 3 @@ -559,7 +579,9 @@ TEMPLATE_TEST_CASE("prim - sparse complete graph K4", auto weight_map = make_vertex_property_map(g, 0); init_shortest_paths(g, weight_map, predecessor); - auto total_wt = prim(g, id_type(10), predecessor, weight_map); + auto total_wt = prim(g, id_type(10), + container_value_fn(weight_map), + container_value_fn(predecessor)); REQUIRE(predecessor[id_type(10)] == id_type(10)); // Root REQUIRE(total_wt == 6); // Same as contiguous K4 test @@ -587,7 +609,9 @@ TEMPLATE_TEST_CASE("prim - sparse kruskal comparison", auto predecessor = make_vertex_property_map(g, id_type{}); auto weight_map = make_vertex_property_map(g, 0); init_shortest_paths(g, weight_map, predecessor); - auto prim_weight = prim(g, id_type(10), predecessor, weight_map); + auto prim_weight = prim(g, id_type(10), + container_value_fn(weight_map), + container_value_fn(predecessor)); REQUIRE(kruskal_weight == prim_weight); } @@ -604,5 +628,8 @@ TEMPLATE_TEST_CASE("prim - sparse invalid seed throws", auto weight_map = make_vertex_property_map(g, 0); init_shortest_paths(g, weight_map, predecessor); - CHECK_THROWS_AS(prim(g, id_type(999), predecessor, weight_map), std::out_of_range); + CHECK_THROWS_AS(prim(g, id_type(999), + container_value_fn(weight_map), + container_value_fn(predecessor)), + std::out_of_range); } From fc0f74dd02d905e05d5691ecce9214490d727a06 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Wed, 25 Mar 2026 19:12:02 -0400 Subject: [PATCH 2/2] Rename and rationalize traversal_common utilities - Rename shortest_path_infinite_distance() -> infinite_distance() - Rename shortest_path_zero() -> zero_distance() - Rename concepts to use 'fn' consistently: distance_function_for -> distance_fn_for predecessor_function_for -> predecessor_fn_for vertex_property_function_for -> vertex_property_fn_for - Remove unused vertex_id_store_t alias and its static_asserts - Update infinite_distance()/zero_distance() in dijkstra and bellman_ford docstrings and inline examples - Update all docs (dijkstra.md, bellman_ford.md, mst.md, connected_components.md, label_propagation.md) to match - Update graph_cpo.hpp comment for raw_vertex_id_t --- docs/user-guide/adjacency-lists.md | 2 +- docs/user-guide/algorithms.md | 6 +- docs/user-guide/algorithms/bellman_ford.md | 45 +++--- .../algorithms/connected_components.md | 45 +++--- docs/user-guide/algorithms/dijkstra.md | 47 +++--- .../algorithms/label_propagation.md | 33 +++-- docs/user-guide/algorithms/mst.md | 28 ++-- include/graph/adj_list/detail/graph_cpo.hpp | 2 +- .../algorithm/bellman_ford_shortest_paths.hpp | 52 +++---- .../graph/algorithm/connected_components.hpp | 111 +++++++------- .../algorithm/dijkstra_shortest_paths.hpp | 24 +-- include/graph/algorithm/label_propagation.hpp | 99 +++++++------ include/graph/algorithm/mst.hpp | 8 +- include/graph/algorithm/traversal_common.hpp | 56 ++++--- .../test_bellman_ford_shortest_paths.cpp | 16 +- .../algorithms/test_connected_components.cpp | 138 +++++++++--------- .../test_dijkstra_shortest_paths.cpp | 28 ++-- tests/algorithms/test_label_propagation.cpp | 54 +++---- tests/algorithms/test_scc_bidirectional.cpp | 30 ++-- .../test_dynamic_graph_integration.cpp | 8 - 20 files changed, 426 insertions(+), 406 deletions(-) diff --git a/docs/user-guide/adjacency-lists.md b/docs/user-guide/adjacency-lists.md index 3ae9062..466a648 100644 --- a/docs/user-guide/adjacency-lists.md +++ b/docs/user-guide/adjacency-lists.md @@ -256,7 +256,7 @@ Extracts the per-vertex value type from a property map container: // Works with any adjacency_list graph — index or mapped auto distances = make_vertex_property_map(g, - shortest_path_infinite_distance()); + infinite_distance()); auto predecessors = make_vertex_property_map>(g, vertex_id_t{}); diff --git a/docs/user-guide/algorithms.md b/docs/user-guide/algorithms.md index ac81eaf..35e19e4 100644 --- a/docs/user-guide/algorithms.md +++ b/docs/user-guide/algorithms.md @@ -73,7 +73,7 @@ dijkstra_shortest_paths(g, 0, distance, predecessor, // Map-based graph: use vertex property maps using G = /* some mapped_adjacency_list graph */; -auto distances = make_vertex_property_map(g, shortest_path_infinite_distance()); +auto distances = make_vertex_property_map(g, infinite_distance()); auto predecessors = make_vertex_property_map>(g, vertex_id_t{}); for (auto&& [uid, u] : views::vertexlist(g)) predecessors[uid] = uid; @@ -299,8 +299,8 @@ All shortest-path algorithms share utilities from `traversal_common.hpp`: |---------|---------| | `init_shortest_paths(distances)` | Set all distances to infinity | | `init_shortest_paths(distances, predecessors)` | Set distances to infinity, predecessors to self | -| `shortest_path_infinite_distance()` | Returns the "infinity" sentinel for type `T` | -| `shortest_path_zero()` | Returns the additive identity for type `T` | +| `infinite_distance()` | Returns the "infinity" sentinel for type `T` | +| `zero_distance()` | Returns the additive identity for type `T` | ### Visitors diff --git a/docs/user-guide/algorithms/bellman_ford.md b/docs/user-guide/algorithms/bellman_ford.md index 5bc8238..7ee0d65 100644 --- a/docs/user-guide/algorithms/bellman_ford.md +++ b/docs/user-guide/algorithms/bellman_ford.md @@ -80,7 +80,7 @@ from the predecessor array. // Multi-source, distances + predecessors [[nodiscard]] constexpr optional> bellman_ford_shortest_paths(G&& g, const Sources& sources, - Distances& distances, Predecessors& predecessors, + DistanceFn&& distance, PredecessorFn&& predecessor, WF&& weight = /* default returns 1 */, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -89,7 +89,7 @@ bellman_ford_shortest_paths(G&& g, const Sources& sources, // Single-source, distances + predecessors [[nodiscard]] constexpr optional> bellman_ford_shortest_paths(G&& g, const vertex_id_t& source, - Distances& distances, Predecessors& predecessors, + DistanceFn&& distance, PredecessorFn&& predecessor, WF&& weight, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -98,7 +98,7 @@ bellman_ford_shortest_paths(G&& g, const vertex_id_t& source, // Multi-source, distances only [[nodiscard]] constexpr optional> bellman_ford_shortest_distances(G&& g, const Sources& sources, - Distances& distances, + DistanceFn&& distance, WF&& weight, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -107,7 +107,7 @@ bellman_ford_shortest_distances(G&& g, const Sources& sources, // Single-source, distances only [[nodiscard]] constexpr optional> bellman_ford_shortest_distances(G&& g, const vertex_id_t& source, - Distances& distances, + DistanceFn&& distance, WF&& weight, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -125,8 +125,8 @@ void find_negative_cycle(G& g, const Predecessors& predecessor, |-----------|-------------| | `g` | Graph satisfying `adjacency_list` | | `source` / `sources` | Source vertex ID or range of source vertex IDs | -| `distances` | Subscriptable by `vertex_id_t`. For index graphs, a pre-sized `std::vector`; for mapped graphs, use `make_vertex_property_map(g, init)`. Must satisfy `vertex_property_map_for`. | -| `predecessors` | Subscriptable by `vertex_id_t`. For index graphs, a pre-sized `std::vector`; for mapped graphs, use `make_vertex_property_map(g, init)`. Must satisfy `vertex_property_map_for`. | +| `distance` | Callable `(const G&, vertex_id_t) -> Distance&` returning a mutable reference to the per-vertex distance. For containers: wrap with `container_value_fn(dist)`. Must satisfy `distance_fn_for`. | +| `predecessor` | Callable `(const G&, vertex_id_t) -> Predecessor&` returning a mutable reference to the per-vertex predecessor. For containers: wrap with `container_value_fn(pred)`. Must satisfy `predecessor_fn_for`. Use `_null_predecessor` when path reconstruction is not needed. | | `weight` | Callable `WF(g, uv)` returning edge weight (may be negative). Must satisfy `basic_edge_weight_function`. | | `visitor` | Optional visitor struct with callback methods (see below). Default: `empty_visitor{}`. | | `compare` | Comparison function for distance values. Default: `std::less<>{}`. | @@ -176,11 +176,12 @@ each edge satisfies the triangle inequality. - ✅ DAGs (works correctly, though topological-order relaxation is faster) - ⚠️ Negative-weight cycles — detected and reported; distances undefined for affected vertices -**Container Requirements:** +**Property Function Requirements:** - Required: `adjacency_list` -- `distances` must satisfy `vertex_property_map_for` -- `predecessors` must satisfy `vertex_property_map_for` +- `distance` must satisfy `distance_fn_for` +- `predecessor` must satisfy `predecessor_fn_for` - `weight` must satisfy `basic_edge_weight_function` +- Use `container_value_fn(vec)` to adapt a `std::vector` or similar container ## Examples @@ -205,12 +206,13 @@ std::vector pred(num_vertices(g)); init_shortest_paths(dist, pred); -auto cycle = bellman_ford_shortest_paths(g, 0u, dist, pred, +auto cycle = bellman_ford_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); if (!cycle) { // No negative cycle — dist[v] is the shortest distance from source 0 // dist[0] = 0, dist[1] = 6, dist[2] = 7, dist[3] = 2, dist[4] = 4 + // Unreachable vertices retain infinite_distance() } ``` @@ -228,7 +230,7 @@ std::vector pred(num_vertices(g)); init_shortest_paths(dist, pred); -auto cycle = bellman_ford_shortest_paths(g, 0u, dist, pred, +auto cycle = bellman_ford_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); if (cycle) { @@ -245,7 +247,7 @@ if (cycle) { Use `find_negative_cycle` to obtain the full cycle from the predecessor array. ```cpp -auto cycle = bellman_ford_shortest_paths(g, 0u, dist, pred, +auto cycle = bellman_ford_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); if (cycle) { @@ -272,7 +274,7 @@ std::vector pred(num_vertices(g)); init_shortest_paths(dist, pred); std::array sources{0u, 3u}; -auto cycle = bellman_ford_shortest_paths(g, sources, dist, pred, +auto cycle = bellman_ford_shortest_paths(g, sources, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // dist[v] = shortest distance from nearest source to v @@ -287,11 +289,12 @@ When you don't need predecessors (and can't reconstruct paths), use std::vector dist(num_vertices(g)); init_shortest_paths(dist); -auto cycle = bellman_ford_shortest_distances(g, 0u, dist, +auto cycle = bellman_ford_shortest_distances(g, 0u, container_value_fn(dist), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); if (!cycle) { // dist[v] = shortest distance from source 0 to v + // Unreachable vertices retain infinite_distance() // No path reconstruction available (no predecessors) } ``` @@ -320,7 +323,7 @@ struct NegativeCycleInspector { }; NegativeCycleInspector inspector; -auto cycle = bellman_ford_shortest_paths(g, 0u, dist, pred, +auto cycle = bellman_ford_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, inspector); @@ -330,24 +333,24 @@ auto cycle = bellman_ford_shortest_paths(g, 0u, dist, pred, ## Mandates - `G` must satisfy `adjacency_list` -- `Distances` must satisfy `vertex_property_map_for` -- `Predecessors` must satisfy `vertex_property_map_for` +- `DistanceFn` must satisfy `distance_fn_for` +- `PredecessorFn` must satisfy `predecessor_fn_for` (or use `_null_predecessor`) - `WF` must satisfy `basic_edge_weight_function` - All overloads are `[[nodiscard]]` — the compiler warns if the return value is discarded ## Preconditions -- `distances` and `predecessors` must be pre-sized for all vertex IDs in `g` -- Call `init_shortest_paths(distances, predecessors)` before invoking the algorithm +- `distance(g, uid)` must be valid for all vertex IDs in `g` +- Call `init_shortest_paths(dist, pred)` on the underlying containers before invoking the algorithm - All source vertex IDs must be valid vertex IDs in `g` - **Always check the return value** — if a negative cycle exists, distances are undefined for affected vertices ## Effects -- Writes shortest-path distances to `distances[v]` for all reachable vertices +- Writes shortest-path distances via `distance(g, v)` for all reachable vertices (when no negative cycle exists) -- Writes predecessor vertex IDs to `predecessors[v]` for path reconstruction +- Writes predecessor vertex IDs via `predecessor(g, v)` for path reconstruction (`bellman_ford_shortest_paths` only) - Does not modify the graph `g` - Invokes visitor callbacks during relaxation and verification passes diff --git a/docs/user-guide/algorithms/connected_components.md b/docs/user-guide/algorithms/connected_components.md index a36d36a..4120427 100644 --- a/docs/user-guide/algorithms/connected_components.md +++ b/docs/user-guide/algorithms/connected_components.md @@ -77,21 +77,24 @@ vertex v. ### `connected_components` — undirected ```cpp -size_t connected_components(G&& g, Component& component); +size_t connected_components(G&& g, ComponentFn&& component); ``` DFS-based algorithm for undirected graphs. Returns the number of connected -components. Assigns `component[v]` = component ID (0-based). +components. `component(g, uid)` is called with each vertex ID to assign its +component ID (0-based). ### `kosaraju` — strongly connected components ```cpp -void kosaraju(G&& g, GT&& g_transpose, Component& component); +void kosaraju(G&& g, GT&& g_transpose, ComponentFn&& component); + +// Bidirectional overload — no transpose graph needed +void kosaraju(G&& g, ComponentFn&& component); ``` -Kosaraju's two-pass DFS algorithm for directed graphs. Requires both the -original graph `g` and its transpose `g_transpose` (edges reversed). Fills -`component[v]` with the SCC ID. +Kosaraju's two-pass DFS algorithm for directed graphs. Fills +`component(g, uid)` with the SCC ID for each vertex. ### `afforest` — union-find with neighbor sampling @@ -105,19 +108,19 @@ void afforest(G&& g, GT&& g_transpose, Component& component, Union-find-based algorithm with neighbor sampling for large or parallel-friendly workloads. The `neighbor_rounds` parameter controls how many initial sampling rounds are performed before falling back to full edge iteration. Call -`compress(component)` afterwards for canonical (root) component IDs on a -single-machine. +`compress(component)` afterwards for canonical (root) component IDs. -The two-graph variant accepts a transpose `g_transpose` for directed-graph -support. The transpose must also satisfy `adjacency_list`. +> **Note:** `afforest` retains a container-based `Component&` interface (not the +> function-based API) because its internal union-find helpers (`link`, `compress`, +> `sample_frequent_element`) require direct subscript access to the component array. ## Parameters | Parameter | Description | |-----------|-------------| | `g` | Graph satisfying `adjacency_list` | -| `g_transpose` | Transpose graph (for `kosaraju` and `afforest` with transpose). `kosaraju` requires `adjacency_list`; `afforest` requires `adjacency_list`. | -| `component` | Subscriptable by `vertex_id_t`. For index graphs, a pre-sized `std::vector`; for mapped graphs, use `make_vertex_property_map(g, init)`. Must satisfy `vertex_property_map_for`. | +| `g_transpose` | Transpose graph (for `kosaraju` and `afforest` with transpose). Must satisfy `adjacency_list`. | +| `component` | For `connected_components` and `kosaraju`: callable `(const G&, vertex_id_t) -> ComponentID&` returning a mutable reference. For containers: wrap with `container_value_fn(comp)`. Must satisfy `vertex_property_fn_for`. For `afforest`: a subscriptable container (`vector` or `unordered_map`), still using the container API. | | `neighbor_rounds` | Number of neighbor-sampling rounds for `afforest` (default: 2) | **Return value (`connected_components` only):** `size_t` — number of connected @@ -143,7 +146,8 @@ components. `kosaraju` and `afforest` return `void`. **Container Requirements:** - Required: `adjacency_list` -- `component` must satisfy `vertex_property_map_for` +- `component` (`connected_components` and `kosaraju`) must satisfy `vertex_property_fn_for` +- `component` (`afforest`) must satisfy `vertex_property_map_for` - `kosaraju`: transpose graph must also satisfy `adjacency_list` ## Examples @@ -167,7 +171,7 @@ using Graph = container::dynamic_graph comp(num_vertices(g)); -size_t num_cc = connected_components(g, comp); +size_t num_cc = connected_components(g, container_value_fn(comp)); // num_cc == 2 // comp[0] == comp[1] == comp[2] (same component) @@ -188,7 +192,7 @@ Graph g({{0, 1}, {1, 2}, {2, 0}, {2, 3}}); Graph g_t({{1, 0}, {2, 1}, {0, 2}, {3, 2}}); std::vector comp(num_vertices(g)); -kosaraju(g, g_t, comp); +kosaraju(g, g_t, container_value_fn(comp)); // comp[0] == comp[1] == comp[2] (cycle forms an SCC) // comp[3] != comp[0] (3 is its own SCC — not on any cycle) @@ -217,7 +221,7 @@ bidirectional edge pairs. undirected_adjacency_list g({{0, 1, 1}, {1, 2, 1}, {3, 4, 1}}); std::vector comp(num_vertices(g)); -size_t num_cc = connected_components(g, comp); +size_t num_cc = connected_components(g, container_value_fn(comp)); // num_cc == 2 // Same results as vov with bidirectional edges, but simpler construction @@ -270,19 +274,22 @@ compress(comp); ## Mandates - `G` must satisfy `adjacency_list` -- `Component` must satisfy `vertex_property_map_for` +- `connected_components` and `kosaraju`: `ComponentFn` must satisfy `vertex_property_fn_for` +- `afforest`: `Component` must satisfy `vertex_property_map_for` - For `kosaraju`: `GT` must satisfy `adjacency_list` ## Preconditions -- `component` must be pre-sized for all vertex IDs in `g` +- `component(g, uid)` must be valid for all vertex IDs in `g` (`connected_components`, `kosaraju`) +- `component` must be pre-sized for all vertex IDs in `g` (`afforest`) - For `connected_components`: undirected graphs must store both directions of each edge (or use `undirected_adjacency_list`) - For `kosaraju`: the transpose graph must contain all edges reversed ## Effects -- Writes component IDs to `component[v]` for all vertices +- Writes component IDs via `component(g, uid)` for all vertices (`connected_components`, `kosaraju`) +- Writes component IDs to `component[uid]` for all vertices (`afforest`) - Does not modify the graph `g` (or `g_transpose`) - For `afforest`: call `compress(component)` afterwards for canonical root IDs diff --git a/docs/user-guide/algorithms/dijkstra.md b/docs/user-guide/algorithms/dijkstra.md index 2067210..e4db6cc 100644 --- a/docs/user-guide/algorithms/dijkstra.md +++ b/docs/user-guide/algorithms/dijkstra.md @@ -82,7 +82,7 @@ function, an optional visitor, and optional comparator/combiner functors. ```cpp // Multi-source, distances + predecessors constexpr void dijkstra_shortest_paths(G&& g, const Sources& sources, - Distances& distances, Predecessors& predecessors, + DistanceFn&& distance, PredecessorFn&& predecessor, WF&& weight = /* default returns 1 */, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -90,7 +90,7 @@ constexpr void dijkstra_shortest_paths(G&& g, const Sources& sources, // Single-source, distances + predecessors constexpr void dijkstra_shortest_paths(G&& g, const vertex_id_t& source, - Distances& distances, Predecessors& predecessors, + DistanceFn&& distance, PredecessorFn&& predecessor, WF&& weight = /* default returns 1 */, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -98,7 +98,7 @@ constexpr void dijkstra_shortest_paths(G&& g, const vertex_id_t& source, // Multi-source, distances only constexpr void dijkstra_shortest_distances(G&& g, const Sources& sources, - Distances& distances, + DistanceFn&& distance, WF&& weight = /* default returns 1 */, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -106,7 +106,7 @@ constexpr void dijkstra_shortest_distances(G&& g, const Sources& sources, // Single-source, distances only constexpr void dijkstra_shortest_distances(G&& g, const vertex_id_t& source, - Distances& distances, + DistanceFn&& distance, WF&& weight = /* default returns 1 */, Visitor&& visitor = empty_visitor(), Compare&& compare = less<>{}, @@ -119,8 +119,8 @@ constexpr void dijkstra_shortest_distances(G&& g, const vertex_id_t& source, |-----------|-------------| | `g` | Graph satisfying `adjacency_list` | | `source` / `sources` | Source vertex ID or range of source vertex IDs | -| `distances` | Subscriptable by `vertex_id_t`. For index graphs, a pre-sized `std::vector`; for mapped graphs, use `make_vertex_property_map(g, init)`. Must satisfy `vertex_property_map_for`. | -| `predecessors` | Subscriptable by `vertex_id_t`. For index graphs, a pre-sized `std::vector`; for mapped graphs, use `make_vertex_property_map(g, init)`. Must satisfy `vertex_property_map_for`. | +| `distance` | Callable `(const G&, vertex_id_t) -> Distance&` returning a mutable reference to the per-vertex distance. For containers: wrap with `container_value_fn(dist)`. Must satisfy `distance_fn_for`. | +| `predecessor` | Callable `(const G&, vertex_id_t) -> Predecessor&` returning a mutable reference to the per-vertex predecessor. For containers: wrap with `container_value_fn(pred)`. Must satisfy `predecessor_fn_for`. Use `_null_predecessor` when path reconstruction is not needed. | | `weight` | Callable `WF(g, uv)` returning edge weight. Must satisfy `basic_edge_weight_function`. Default: returns `1` for every edge (unweighted). | | `visitor` | Optional visitor struct with callback methods (see below). Default: `empty_visitor{}`. | | `compare` | Comparison function for distance values. Default: `std::less<>{}`. | @@ -160,11 +160,12 @@ need to define the events you care about — missing methods are silently skippe - ✅ Disconnected graphs (unreachable vertices retain infinity distance) - ✅ Empty graphs (no-op) -**Container Requirements:** +**Property Function Requirements:** - Required: `adjacency_list` -- `distances` must satisfy `vertex_property_map_for` -- `predecessors` must satisfy `vertex_property_map_for` +- `distance` must satisfy `distance_fn_for` +- `predecessor` must satisfy `predecessor_fn_for` - `weight` must satisfy `basic_edge_weight_function` +- Use `container_value_fn(vec)` to adapt a `std::vector` or similar container ## Examples @@ -201,7 +202,7 @@ std::vector pred(num_vertices(g)); // Initialize distances to infinity, predecessors to self init_shortest_paths(dist, pred); -dijkstra_shortest_paths(g, 0u, dist, pred, +dijkstra_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // dist[0] = 0, dist[1] = 8, dist[2] = 5, dist[3] = 6, dist[4] = 2 @@ -217,11 +218,11 @@ When you only need distances and don't need to reconstruct paths, use std::vector dist(num_vertices(g)); init_shortest_paths(dist); // one-argument form: distances only -dijkstra_shortest_distances(g, 0u, dist, +dijkstra_shortest_distances(g, 0u, container_value_fn(dist), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // dist[v] = shortest distance from source 0 to v -// Unreachable vertices remain at numeric_limits::max() +// Unreachable vertices remain at infinite_distance() ``` ### Example 3: Multi-Source Shortest Paths @@ -237,7 +238,7 @@ init_shortest_paths(dist, pred); // Sources can be any range: vector, array, initializer_list std::array sources{0u, 2u}; -dijkstra_shortest_paths(g, sources, dist, pred, +dijkstra_shortest_paths(g, sources, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }); // dist[v] = shortest distance from the nearest source to v @@ -250,12 +251,12 @@ After running `dijkstra_shortest_paths`, reconstruct the shortest path from source to any target by walking the predecessor array backwards. ```cpp -// After running dijkstra_shortest_paths(g, source, dist, pred, weight) ... +// After running dijkstra_shortest_paths(g, source, container_value_fn(dist), container_value_fn(pred), weight) ... unsigned target = 3; unsigned source = 0; -if (dist[target] == std::numeric_limits::max()) { +if (dist[target] == infinite_distance()) { // target is unreachable from source } else { // Build path in reverse @@ -289,7 +290,7 @@ std::vector pred(num_vertices(g)); init_shortest_paths(dist, pred); // No weight function — default returns 1 per edge -dijkstra_shortest_paths(g, 0u, dist, pred); +dijkstra_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred)); // dist[0] = 0, dist[1] = 1, dist[2] = 1, dist[3] = 2, dist[4] = 3 // These are hop counts — same as BFS distances @@ -324,7 +325,7 @@ std::vector pred(num_vertices(g)); init_shortest_paths(dist, pred); RelaxationTracker tracker; -dijkstra_shortest_paths(g, 0u, dist, pred, +dijkstra_shortest_paths(g, 0u, container_value_fn(dist), container_value_fn(pred), [](const auto& g, const auto& uv) { return edge_value(g, uv); }, tracker); @@ -348,8 +349,8 @@ struct IdVisitor { ## Mandates - `G` must satisfy `adjacency_list` -- `Distances` must satisfy `vertex_property_map_for` -- `Predecessors` must satisfy `vertex_property_map_for` +- `DistanceFn` must satisfy `distance_fn_for` +- `PredecessorFn` must satisfy `predecessor_fn_for` (or use `_null_predecessor`) - `WF` must satisfy `basic_edge_weight_function` - Visitor callbacks (if present) must accept appropriate graph and vertex/edge parameters @@ -357,14 +358,14 @@ struct IdVisitor { - All edge weights must be **non-negative**. For signed weight types, a negative weight throws `std::out_of_range` at runtime. -- `distances` and `predecessors` must be pre-sized for all vertex IDs in `g` -- Call `init_shortest_paths(distances, predecessors)` before invoking the algorithm +- `distance(g, uid)` must be valid for all vertex IDs in `g` +- Call `init_shortest_paths(distances, predecessors)` on the underlying containers before invoking the algorithm - All source vertex IDs must be valid vertex IDs in `g` ## Effects -- Writes shortest-path distances to `distances[v]` for all reachable vertices -- Writes predecessor vertex IDs to `predecessors[v]` for path reconstruction +- Writes shortest-path distances via `distance(g, v)` for all reachable vertices +- Writes predecessor vertex IDs via `predecessor(g, v)` for path reconstruction (`dijkstra_shortest_paths` only) - Does not modify the graph `g` - Invokes visitor callbacks in Dijkstra traversal order diff --git a/docs/user-guide/algorithms/label_propagation.md b/docs/user-guide/algorithms/label_propagation.md index c124226..e469339 100644 --- a/docs/user-guide/algorithms/label_propagation.md +++ b/docs/user-guide/algorithms/label_propagation.md @@ -83,13 +83,13 @@ Key behaviors: ```cpp // Basic: all vertices start with labels -void label_propagation(G&& g, Label& label, +void label_propagation(G&& g, LabelFn&& label, Gen&& rng = /* default */, T max_iters = std::numeric_limits::max()); // With empty-label sentinel: unlabeled vertices don't participate -void label_propagation(G&& g, Label& label, - range_value_t