Skip to content

Commit 56d193c

Browse files
authored
Replace shortest_paths.hpp with dijkstra_shortest_paths.hpp (#125)
New implementation includes a visitor.
1 parent 55b0771 commit 56d193c

File tree

4 files changed

+391
-370
lines changed

4 files changed

+391
-370
lines changed

example/CppCon2021/examples/ospf.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#include <list>
1111

1212
//#include "dijkstra.hpp"
13-
#include "graph/algorithm/shortest_paths.hpp"
13+
#include "graph/algorithm/dijkstra_shortest_paths.hpp"
1414
#include "ospf-graph.hpp"
1515
#include "utilities.hpp"
1616

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/**
2+
* @file dijkstra_shortest_paths.hpp
3+
*
4+
* @brief Single-Source Shortest paths and shortest distances algorithms using Dijkstra's algorithm.
5+
*
6+
* @copyright Copyright (c) 2024
7+
*
8+
* SPDX-License-Identifier: BSL-1.0
9+
*
10+
* @authors
11+
* Andrew Lumsdaine
12+
* Phil Ratzloff
13+
*/
14+
15+
#include <queue>
16+
#include <vector>
17+
#include <ranges>
18+
#include <functional>
19+
#include <algorithm>
20+
#include <fmt/format.h>
21+
#include "graph/graph.hpp"
22+
#include "graph/views/incidence.hpp"
23+
24+
#define NEW_DIJKSTRA
25+
26+
#ifndef GRAPH_DIJKSTRA_SHORTEST_PATHS_HPP
27+
# define GRAPH_DIJKSTRA_SHORTEST_PATHS_HPP
28+
29+
namespace std::graph {
30+
31+
template <class G, class WF, class DistanceValue, class Compare, class Combine> // For exposition only
32+
concept basic_edge_weight_function = // e.g. weight(uv)
33+
is_arithmetic_v<DistanceValue> && strict_weak_order<Compare, DistanceValue, DistanceValue> &&
34+
assignable_from<add_lvalue_reference_t<DistanceValue>,
35+
invoke_result_t<Combine, DistanceValue, invoke_result_t<WF, edge_reference_t<G>>>>;
36+
37+
template <class G, class WF, class DistanceValue> // For exposition only
38+
concept edge_weight_function = // e.g. weight(uv)
39+
is_arithmetic_v<invoke_result_t<WF, edge_reference_t<G>>> &&
40+
basic_edge_weight_function<G, WF, DistanceValue, less<DistanceValue>, plus<DistanceValue>>;
41+
42+
template <adjacency_list G>
43+
class dijkstra_visitor_base {
44+
// Types
45+
public:
46+
using graph_type = G;
47+
using vertex_desc_type = vertex_descriptor<vertex_id_t<G>, vertex_reference_t<G>, void>;
48+
using sourced_edge_desc_type = edge_descriptor<vertex_id_t<G>, true, edge_reference_t<G>, void>;
49+
50+
// Visitor Functions
51+
public:
52+
// vertex visitor functions
53+
constexpr void on_initialize_vertex(vertex_desc_type&& vdesc) {}
54+
constexpr void on_discover_vertex(vertex_desc_type&& vdesc) {}
55+
constexpr void on_examine_vertex(vertex_desc_type&& vdesc) {}
56+
constexpr void on_finish_vertex(vertex_desc_type&& vdesc) {}
57+
58+
// edge visitor functions
59+
constexpr void on_examine_edge(sourced_edge_desc_type&& edesc) {}
60+
constexpr void on_edge_relaxed(sourced_edge_desc_type&& edesc) {}
61+
constexpr void on_edge_not_relaxed(sourced_edge_desc_type&& edesc) {}
62+
};
63+
64+
template <class G, class Visitor>
65+
concept dijkstra_visitor = //is_arithmetic<typename Visitor::distance_type> &&
66+
requires(Visitor& v, Visitor::vertex_desc_type& vdesc, Visitor::sourced_edge_desc_type& edesc) {
67+
//typename Visitor::distance_type;
68+
69+
{ v.on_initialize_vertex(vdesc) };
70+
{ v.on_discover_vertex(vdesc) };
71+
{ v.on_examine_vertex(vdesc) };
72+
{ v.on_finish_vertex(vdesc) };
73+
74+
{ v.on_examine_edge(edesc) };
75+
{ v.on_edge_relaxed(edesc) };
76+
{ v.on_edge_not_relaxed(edesc) };
77+
};
78+
79+
/**
80+
* @ingroup graph_algorithms
81+
* @brief Returns a value to define an invalid distance used to initialize distance values
82+
* in the distance range before one of the shorts paths functions.
83+
*
84+
* @tparam DistanceValue The type of the distance.
85+
*
86+
* @return A unique sentinal value to indicate that a value is invalid, or undefined.
87+
*/
88+
template <class DistanceValue>
89+
constexpr auto shortest_path_invalid_distance() {
90+
return numeric_limits<DistanceValue>::max();
91+
}
92+
93+
/**
94+
* @ingroup graph_algorithms
95+
* @brief Returns a distance value of zero.
96+
*
97+
* @tparam DistanceValue The type of the distance.
98+
*
99+
* @return A value of zero distance.
100+
*/
101+
template <class DistanceValue>
102+
constexpr auto shortest_path_zero() {
103+
return DistanceValue();
104+
}
105+
106+
/**
107+
* @ingroup graph_algorithms
108+
* @brief Intializes the distance values to shortest_path_invalid_distance().
109+
*
110+
* @tparam Distances The range type of the distances.
111+
*
112+
* @param distances The range of distance values to initialize.
113+
*/
114+
template <class Distances>
115+
constexpr void init_shortest_paths(Distances& distances) {
116+
ranges::fill(distances, shortest_path_invalid_distance<ranges::range_value_t<Distances>>());
117+
}
118+
119+
/**
120+
* @ingroup graph_algorithms
121+
* @brief Intializes the distance and predecessor values for shortest paths algorithms.
122+
*
123+
* @tparam Distances The range type of the distances.
124+
* @tparam Predecessors The range type of the predecessors.
125+
*
126+
* @param distances The range of distance values to initialize.
127+
* @param predecessors The range of predecessors to initialize.
128+
*/
129+
template <class Distances, class Predecessors>
130+
constexpr void init_shortest_paths(Distances& distances, Predecessors& predecessors) {
131+
init_shortest_paths(distances);
132+
133+
using pred_t = ranges::range_value_t<Predecessors>;
134+
pred_t i = pred_t();
135+
for (auto& pred : predecessors)
136+
pred = i++;
137+
}
138+
139+
/**
140+
* @brief An always-empty random_access_range.
141+
*
142+
* A unique range type that can be used at compile time to determine if predecessors need to
143+
* be evaluated.
144+
*
145+
* This is not in the P1709 proposal. It's a quick hack to allow us to implement quickly.
146+
*/
147+
class _null_range_type : public std::vector<size_t> {
148+
using T = size_t;
149+
using Allocator = std::allocator<T>;
150+
using Base = std::vector<T, Allocator>;
151+
152+
public:
153+
_null_range_type() noexcept(noexcept(Allocator())) = default;
154+
explicit _null_range_type(const Allocator& alloc) noexcept {}
155+
_null_range_type(Base::size_type count, const T& value, const Allocator& alloc = Allocator()) {}
156+
explicit _null_range_type(Base::size_type count, const Allocator& alloc = Allocator()) {}
157+
template <class InputIt>
158+
_null_range_type(InputIt first, InputIt last, const Allocator& alloc = Allocator()) {}
159+
_null_range_type(const _null_range_type& other) : Base() {}
160+
_null_range_type(const _null_range_type& other, const Allocator& alloc) {}
161+
_null_range_type(_null_range_type&& other) noexcept {}
162+
_null_range_type(_null_range_type&& other, const Allocator& alloc) {}
163+
_null_range_type(std::initializer_list<T> init, const Allocator& alloc = Allocator()) {}
164+
};
165+
166+
inline static _null_range_type _null_predecessors;
167+
168+
/**
169+
* @brief Dijkstra's single-source shortest paths algorithm with a visitor.
170+
*
171+
* The implementation was taken from boost::graph dijkstra_shortes_paths_no_init.
172+
*
173+
* Pre-conditions:
174+
* - predecessors has been initialized with init_shortest_paths().
175+
* - distances has been initialized with init_shortest_paths().
176+
* - The weight function must return a value that can be compared (e.g. <) with the Distance
177+
* type and combined (e.g. +) with the Distance type.
178+
* - The visitor must implement the dijkstra_visitor concept and is typically derived from
179+
* dijkstra_visitor_base.
180+
*
181+
* Throws:
182+
* - out_of_range if the source vertex is out of range.
183+
* - graph_error if a negative edge weight is encountered.
184+
* - logic_error if an edge to a new vertex was not relaxed.
185+
*
186+
* @tparam G The graph type,
187+
* @tparam Distances The distance random access range.
188+
* @tparam Predecessors The predecessor random access range.
189+
* @tparam WF Edge weight function. Defaults to a function that returns 1.
190+
* @tparam Visitor Visitor type with functions called for different events in the algorithm.
191+
* Function calls are removed by the optimizer if not uesd.
192+
* @tparam Compare Comparison function for Distance values. Defaults to less<DistanceValue>.
193+
* @tparam Combine Combine function for Distance values. Defaults to plus<DistanctValue>.
194+
*/
195+
template <index_adjacency_list G,
196+
ranges::random_access_range Distances,
197+
ranges::random_access_range Predecessors,
198+
class WF = std::function<ranges::range_value_t<Distances>(edge_reference_t<G>)>,
199+
class Visitor = dijkstra_visitor_base<G>,
200+
class Compare = less<ranges::range_value_t<Distances>>,
201+
class Combine = plus<ranges::range_value_t<Distances>>>
202+
requires is_arithmetic_v<ranges::range_value_t<Distances>> && //
203+
convertible_to<vertex_id_t<G>, ranges::range_value_t<Predecessors>> &&
204+
basic_edge_weight_function<G, WF, ranges::range_value_t<Distances>, Compare, Combine>
205+
// && dijkstra_visitor<G, Visitor>
206+
void dijkstra_shortest_paths(
207+
G& g,
208+
const vertex_id_t<G> source,
209+
Distances& distances,
210+
Predecessors& predecessor,
211+
WF&& weight =
212+
[](edge_reference_t<G> uv) { return ranges::range_value_t<Distances>(1); }, // default weight(uv) -> 1
213+
Visitor&& visitor = dijkstra_visitor_base<G>(),
214+
Compare&& compare = less<ranges::range_value_t<Distances>>(),
215+
Combine&& combine = plus<ranges::range_value_t<Distances>>()) {
216+
using id_type = vertex_id_t<G>;
217+
using DistanceValue = ranges::range_value_t<Distances>;
218+
using weight_type = invoke_result_t<WF, edge_reference_t<G>>;
219+
220+
// relxing the target is the function of reducing the distance from the source to the target
221+
auto relax_target = [&g, &predecessor, &distances, &compare, &combine] //
222+
(edge_reference_t<G> e, vertex_id_t<G> uid, const weight_type& w_e) -> bool {
223+
id_type vid = target_id(g, e);
224+
const DistanceValue d_u = distances[static_cast<size_t>(uid)];
225+
const DistanceValue d_v = distances[static_cast<size_t>(vid)];
226+
227+
if (compare(combine(d_u, w_e), d_v)) {
228+
distances[static_cast<size_t>(vid)] = combine(d_u, w_e);
229+
if constexpr (!is_same_v<Predecessors, _null_range_type>) {
230+
predecessor[static_cast<size_t>(vid)] = uid;
231+
}
232+
return true;
233+
}
234+
return false;
235+
};
236+
237+
constexpr auto zero = shortest_path_zero<DistanceValue>();
238+
constexpr auto infinite = shortest_path_invalid_distance<DistanceValue>();
239+
240+
const id_type N = static_cast<id_type>(num_vertices(g));
241+
242+
auto qcompare = [&distances](id_type a, id_type b) {
243+
return distances[static_cast<size_t>(a)] > distances[static_cast<size_t>(b)];
244+
};
245+
using Queue = std::priority_queue<id_type, vector<id_type>, decltype(qcompare)>;
246+
Queue queue(qcompare);
247+
248+
// (The optimizer removes this loop if on_initialize_vertex() is empty.)
249+
for (id_type uid = 0; uid < N; ++uid) {
250+
visitor.on_initialize_vertex({uid, *find_vertex(g, uid)});
251+
}
252+
253+
// Seed the queue with the initial vertex
254+
if (source >= N || source < 0) {
255+
throw out_of_range(fmt::format("dijkstra_shortest_paths: source vertex id of '{}' is out of range", source));
256+
}
257+
queue.push(source);
258+
distances[static_cast<size_t>(source)] = zero; // mark source as discovered
259+
visitor.on_discover_vertex({source, *find_vertex(g, source)});
260+
261+
// Main loop to process the queue
262+
while (!queue.empty()) {
263+
const id_type uid = queue.top();
264+
queue.pop();
265+
visitor.on_examine_vertex({uid, *find_vertex(g, uid)});
266+
267+
// Process all outgoing edges from the current vertex
268+
for (auto&& [vid, uv, w] : views::incidence(g, uid, weight)) {
269+
visitor.on_examine_edge({uid, vid, uv});
270+
271+
// Negative weights are not allowed for Dijkstra's algorithm
272+
if constexpr (is_signed_v<weight_type>) {
273+
if (w < zero) {
274+
throw graph_error(
275+
fmt::format("dijkstra_shortest_paths: invalid negative edge weight of '{}' encountered", w));
276+
}
277+
}
278+
279+
const bool is_neighbor_undiscovered = (distances[static_cast<size_t>(vid)] == infinite);
280+
const bool was_edge_relaxed = relax_target(uv, uid, w);
281+
282+
if (is_neighbor_undiscovered) {
283+
// tree_edge
284+
if (was_edge_relaxed) {
285+
visitor.on_edge_relaxed({uid, vid, uv});
286+
visitor.on_discover_vertex({vid, *find_vertex(g, vid)});
287+
queue.push(vid);
288+
} else {
289+
// This is an indicator of a bug in the algorithm and should be investigated.
290+
throw logic_error("dijkstra_shortest_paths: unexpected state where an edge to a new vertex was not relaxed");
291+
}
292+
} else {
293+
// non-tree edge
294+
if (was_edge_relaxed) {
295+
visitor.on_edge_relaxed({uid, vid, uv});
296+
queue.push(vid); // re-enqueue vid to re-evaluate its neighbors with a shorter path
297+
} else {
298+
visitor.on_edge_not_relaxed({uid, vid, uv});
299+
}
300+
}
301+
}
302+
303+
// Note: while we *think* we're done with this vertex, we may not be. If the graph is unbalanced
304+
// and another path to this vertex has a lower accumulated weight, we'll process it again.
305+
// A consequence is that examine_vertex could be called twice (or more) on the same vertex.
306+
visitor.on_finish_vertex({uid, *find_vertex(g, uid)});
307+
} // while(!queue.empty())
308+
}
309+
310+
/**
311+
* @brief Shortest distnaces from a single source using Dijkstra's single-source shortest paths algorithm
312+
* with a visitor.
313+
*
314+
* This is identical to dijkstra_shortest_paths() except that it does not require a predecessors range.
315+
*
316+
* Pre-conditions:
317+
* - distances has been initialized with init_shortest_paths().
318+
* - The weight function must return a value that can be compared (e.g. <) with the Distance
319+
* type and combined (e.g. +) with the Distance type.
320+
* - The visitor must implement the dijkstra_visitor concept and is typically derived from
321+
* dijkstra_visitor_base.
322+
*
323+
* Throws:
324+
* - out_of_range if the source vertex is out of range.
325+
* - graph_error if a negative edge weight is encountered.
326+
* - logic_error if an edge to a new vertex was not relaxed.
327+
*
328+
* @tparam G The graph type,
329+
* @tparam Distances The distance random access range.
330+
* @tparam WF Edge weight function. Defaults to a function that returns 1.
331+
* @tparam Visitor Visitor type with functions called for different events in the algorithm.
332+
* Function calls are removed by the optimizer if not uesd.
333+
* @tparam Compare Comparison function for Distance values. Defaults to less<DistanceValue>.
334+
* @tparam Combine Combine function for Distance values. Defaults to plus<DistanctValue>.
335+
*/
336+
template <index_adjacency_list G,
337+
ranges::random_access_range Distances,
338+
class WF = std::function<ranges::range_value_t<Distances>(edge_reference_t<G>)>,
339+
class Visitor = dijkstra_visitor_base<G>,
340+
class Compare = less<ranges::range_value_t<Distances>>,
341+
class Combine = plus<ranges::range_value_t<Distances>>>
342+
requires is_arithmetic_v<ranges::range_value_t<Distances>> && //
343+
basic_edge_weight_function<G, WF, ranges::range_value_t<Distances>, Compare, Combine>
344+
//&& dijkstra_visitor<G, Visitor>
345+
void dijkstra_shortest_distances(
346+
G& g,
347+
const vertex_id_t<G> source,
348+
Distances& distances,
349+
WF&& weight =
350+
[](edge_reference_t<G> uv) { return ranges::range_value_t<Distances>(1); }, // default weight(uv) -> 1
351+
Visitor&& visitor = dijkstra_visitor_base<G>(),
352+
Compare&& compare = less<ranges::range_value_t<Distances>>(),
353+
Combine&& combine = plus<ranges::range_value_t<Distances>>()) {
354+
dijkstra_shortest_paths(g, source, distances, _null_predecessors, forward<WF>(weight), std::forward<Visitor>(visitor),
355+
std::forward<Compare>(compare), std::forward<Combine>(combine));
356+
}
357+
358+
} // namespace std::graph
359+
360+
#endif // GRAPH_DIJKSTRA_SHORTEST_PATHS_HPP

0 commit comments

Comments
 (0)