Skip to content

Commit 215d516

Browse files
authored
Bellman (#126)
* Add bellman_ford_shortest_paths impl * Add Bellman-Ford unit tests Negative weight cycles not tested * distances[source] wasn't initialized
1 parent c9b7c5d commit 215d516

6 files changed

+1086
-110
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* @file bellman_ford_shortest_paths.hpp
3+
*
4+
* @brief Single-Source Shortest paths and shortest distances algorithms using Bellman-Ford'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 "graph/graph.hpp"
16+
#include "graph/views/incidence.hpp"
17+
#include "graph/views/edgelist.hpp"
18+
#include "graph/algorithm/common_shortest_paths.hpp"
19+
20+
#include <ranges>
21+
#include <fmt/format.h>
22+
23+
#ifndef GRAPH_BELLMAN_SHORTEST_PATHS_HPP
24+
# define GRAPH_BELLMAN_SHORTEST_PATHS_HPP
25+
26+
//# define ENABLE_EVAL_NEG_WEIGHT_CYCLE 1 // for debugging
27+
28+
namespace std::graph {
29+
30+
template <adjacency_list G>
31+
class bellman_visitor_base {
32+
// Types
33+
public:
34+
using graph_type = G;
35+
using vertex_desc_type = vertex_descriptor<vertex_id_t<G>, vertex_reference_t<G>, void>;
36+
using sourced_edge_desc_type = edge_descriptor<vertex_id_t<G>, true, edge_reference_t<G>, void>;
37+
38+
// Visitor Functions
39+
public:
40+
// vertex visitor functions
41+
//constexpr void on_initialize_vertex(vertex_desc_type&& vdesc) {}
42+
//constexpr void on_discover_vertex(vertex_desc_type&& vdesc) {}
43+
//constexpr void on_examine_vertex(vertex_desc_type&& vdesc) {}
44+
//constexpr void on_finish_vertex(vertex_desc_type&& vdesc) {}
45+
46+
// edge visitor functions
47+
constexpr void on_examine_edge(sourced_edge_desc_type&& edesc) {}
48+
constexpr void on_edge_relaxed(sourced_edge_desc_type&& edesc) {}
49+
constexpr void on_edge_not_relaxed(sourced_edge_desc_type&& edesc) {}
50+
constexpr void on_edge_minimized(sourced_edge_desc_type&& edesc) {}
51+
constexpr void on_edge_not_minimized(sourced_edge_desc_type&& edesc) {}
52+
};
53+
54+
template <class G, class Visitor>
55+
concept bellman_visitor = //is_arithmetic<typename Visitor::distance_type> &&
56+
requires(Visitor& v, Visitor::vertex_desc_type& vdesc, Visitor::sourced_edge_desc_type& edesc) {
57+
//typename Visitor::distance_type;
58+
59+
//{ v.on_initialize_vertex(vdesc) };
60+
//{ v.on_discover_vertex(vdesc) };
61+
//{ v.on_examine_vertex(vdesc) };
62+
//{ v.on_finish_vertex(vdesc) };
63+
64+
{ v.on_examine_edge(edesc) };
65+
{ v.on_edge_relaxed(edesc) };
66+
{ v.on_edge_not_relaxed(edesc) };
67+
{ v.on_edge_minimized(edesc) };
68+
{ v.on_edge_not_minimized(edesc) };
69+
};
70+
71+
/**
72+
* @brief Bellman-Ford's single-source shortest paths algorithm with a visitor.
73+
*
74+
* The implementation was taken from boost::graph bellman_ford_shortest_paths.
75+
*
76+
* Complexity: O(V * E)
77+
*
78+
* Pre-conditions:
79+
* - 0 <= source < num_vertices(g)
80+
* - predecessors has been initialized with init_shortest_paths().
81+
* - distances has been initialized with init_shortest_paths().
82+
* - The weight function must return a value that can be compared (e.g. <) with the Distance
83+
* type and combined (e.g. +) with the Distance type.
84+
* - The visitor must implement the bellman_visitor concept and is typically derived from
85+
* bellman_visitor_base.
86+
*
87+
* Throws:
88+
* - out_of_range if the source vertex is out of range.
89+
*
90+
* @tparam G The graph type,
91+
* @tparam Distances The distance random access range.
92+
* @tparam Predecessors The predecessor random access range.
93+
* @tparam WF Edge weight function. Defaults to a function that returns 1.
94+
* @tparam Visitor Visitor type with functions called for different events in the algorithm.
95+
* Function calls are removed by the optimizer if not uesd.
96+
* @tparam Compare Comparison function for Distance values. Defaults to less<DistanceValue>.
97+
* @tparam Combine Combine function for Distance values. Defaults to plus<DistanctValue>.
98+
*
99+
* @return true if all edges were minimized, false if a negative weight cycle was found. If an edge
100+
* was not minimized, the on_edge_not_minimized event is called.
101+
*/
102+
template <index_adjacency_list G,
103+
ranges::random_access_range Distances,
104+
ranges::random_access_range Predecessors,
105+
class WF = std::function<ranges::range_value_t<Distances>(edge_reference_t<G>)>,
106+
class Visitor = bellman_visitor_base<G>,
107+
class Compare = less<ranges::range_value_t<Distances>>,
108+
class Combine = plus<ranges::range_value_t<Distances>>>
109+
requires is_arithmetic_v<ranges::range_value_t<Distances>> && //
110+
convertible_to<vertex_id_t<G>, ranges::range_value_t<Predecessors>> &&
111+
basic_edge_weight_function<G, WF, ranges::range_value_t<Distances>, Compare, Combine>
112+
// && bellman_visitor<G, Visitor>
113+
bool bellman_ford_shortest_paths(
114+
G& g,
115+
const vertex_id_t<G> source,
116+
Distances& distances,
117+
Predecessors& predecessor,
118+
WF&& weight =
119+
[](edge_reference_t<G> uv) { return ranges::range_value_t<Distances>(1); }, // default weight(uv) -> 1
120+
Visitor&& visitor = bellman_visitor_base<G>(),
121+
Compare&& compare = less<ranges::range_value_t<Distances>>(),
122+
Combine&& combine = plus<ranges::range_value_t<Distances>>()) {
123+
using id_type = vertex_id_t<G>;
124+
using DistanceValue = ranges::range_value_t<Distances>;
125+
using weight_type = invoke_result_t<WF, edge_reference_t<G>>;
126+
127+
// relxing the target is the function of reducing the distance from the source to the target
128+
auto relax_target = [&g, &predecessor, &distances, &compare, &combine] //
129+
(edge_reference_t<G> e, vertex_id_t<G> uid, const weight_type& w_e) -> bool {
130+
id_type vid = target_id(g, e);
131+
const DistanceValue d_u = distances[static_cast<size_t>(uid)];
132+
const DistanceValue d_v = distances[static_cast<size_t>(vid)];
133+
134+
if (compare(combine(d_u, w_e), d_v)) {
135+
distances[static_cast<size_t>(vid)] = combine(d_u, w_e);
136+
if constexpr (!is_same_v<Predecessors, _null_range_type>) {
137+
predecessor[static_cast<size_t>(vid)] = uid;
138+
}
139+
return true;
140+
}
141+
return false;
142+
};
143+
144+
constexpr auto zero = shortest_path_zero<DistanceValue>();
145+
constexpr auto infinite = shortest_path_invalid_distance<DistanceValue>();
146+
147+
const id_type N = static_cast<id_type>(num_vertices(g));
148+
149+
distances[source] = zero;
150+
if (source >= N || source < 0) {
151+
throw out_of_range(fmt::format("bellman_fored_shortest_paths: source vertex id of '{}' is out of range", source));
152+
}
153+
154+
for (id_type k = 0; k < N; ++k) {
155+
bool at_least_one_edge_relaxed = false;
156+
for (auto&& [uid, vid, uv, w] : views::edgelist(g, weight)) {
157+
visitor.on_examine_edge({uid, vid, uv});
158+
if (relax_target(uv, uid, w)) {
159+
at_least_one_edge_relaxed = true;
160+
visitor.on_edge_relaxed({uid, vid, uv});
161+
} else
162+
visitor.on_edge_not_relaxed({uid, vid, uv});
163+
}
164+
if (!at_least_one_edge_relaxed)
165+
break;
166+
}
167+
168+
// Check for negative weight cycles
169+
for (auto&& [uid, vid, uv, w] : views::edgelist(g, weight)) {
170+
if (compare(combine(distances[uid], w), distances[vid])) {
171+
visitor.on_edge_not_minimized({uid, vid, uv});
172+
173+
# if ENABLE_EVAL_NEG_WEIGHT_CYCLE // for debugging
174+
// A negative cycle exists; find a vertex on the cycle
175+
// (Thanks to Wikipedia's Bellman-Ford article for this code)
176+
predecessor[vid] = uid;
177+
vector<bool> visited(num_vertices(g), false);
178+
visited[vid] = true;
179+
while (!visited[uid]) {
180+
visited[uid] = true;
181+
uid = predecessor[uid];
182+
}
183+
// uid is a vertex in a negative cycle, fill ncycle with the vertices in the cycle
184+
vector<id_type> ncycle;
185+
ncycle.push_back(uid);
186+
vid = predecessor[uid];
187+
while (vid != uid) {
188+
ncycle.push_back(vid);
189+
vid = predecessor[vid]
190+
}
191+
# endif
192+
return false;
193+
} else {
194+
visitor.on_edge_minimized({uid, vid, uv});
195+
}
196+
}
197+
198+
return true;
199+
}
200+
201+
/**
202+
* @brief Shortest distnaces from a single source using Bellman-Ford's single-source shortest paths
203+
* algorithm with a visitor.
204+
*
205+
* This is identical to bellman_ford_shortest_paths() except that it does not require a predecessors range.
206+
*
207+
* Complexity: O(V * E)
208+
*
209+
* Pre-conditions:
210+
* - distances has been initialized with init_shortest_paths().
211+
* - The weight function must return a value that can be compared (e.g. <) with the Distance
212+
* type and combined (e.g. +) with the Distance type.
213+
* - The visitor must implement the bellman_visitor concept and is typically derived from
214+
* bellman_visitor_base.
215+
*
216+
* Throws:
217+
* - out_of_range if the source vertex is out of range.
218+
*
219+
* @tparam G The graph type,
220+
* @tparam Distances The distance random access range.
221+
* @tparam WF Edge weight function. Defaults to a function that returns 1.
222+
* @tparam Visitor Visitor type with functions called for different events in the algorithm.
223+
* Function calls are removed by the optimizer if not uesd.
224+
* @tparam Compare Comparison function for Distance values. Defaults to less<DistanceValue>.
225+
* @tparam Combine Combine function for Distance values. Defaults to plus<DistanctValue>.
226+
*
227+
* @return true if all edges were minimized, false if a negative weight cycle was found. If an edge
228+
* was not minimized, the on_edge_not_minimized event is called.
229+
*/
230+
template <index_adjacency_list G,
231+
ranges::random_access_range Distances,
232+
class WF = std::function<ranges::range_value_t<Distances>(edge_reference_t<G>)>,
233+
class Visitor = bellman_visitor_base<G>,
234+
class Compare = less<ranges::range_value_t<Distances>>,
235+
class Combine = plus<ranges::range_value_t<Distances>>>
236+
requires is_arithmetic_v<ranges::range_value_t<Distances>> && //
237+
basic_edge_weight_function<G, WF, ranges::range_value_t<Distances>, Compare, Combine>
238+
//&& bellman_visitor<G, Visitor>
239+
bool bellman_ford_shortest_distances(
240+
G& g,
241+
const vertex_id_t<G> source,
242+
Distances& distances,
243+
WF&& weight =
244+
[](edge_reference_t<G> uv) { return ranges::range_value_t<Distances>(1); }, // default weight(uv) -> 1
245+
Visitor&& visitor = bellman_visitor_base<G>(),
246+
Compare&& compare = less<ranges::range_value_t<Distances>>(),
247+
Combine&& combine = plus<ranges::range_value_t<Distances>>()) {
248+
return bellman_ford_shortest_paths(g, source, distances, _null_predecessors, forward<WF>(weight),
249+
std::forward<Visitor>(visitor), std::forward<Compare>(compare),
250+
std::forward<Combine>(combine));
251+
}
252+
253+
} // namespace std::graph
254+
255+
#endif // GRAPH_BELLMAN_SHORTEST_PATHS_HPP
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
5+
#ifndef GRAPH_COMMON_SHORTEST_PATHS_HPP
6+
# define GRAPH_COMMON_SHORTEST_PATHS_HPP
7+
8+
namespace std::graph {
9+
template <class G, class WF, class DistanceValue, class Compare, class Combine> // For exposition only
10+
concept basic_edge_weight_function = // e.g. weight(uv)
11+
is_arithmetic_v<DistanceValue> && strict_weak_order<Compare, DistanceValue, DistanceValue> &&
12+
assignable_from<add_lvalue_reference_t<DistanceValue>,
13+
invoke_result_t<Combine, DistanceValue, invoke_result_t<WF, edge_reference_t<G>>>>;
14+
15+
template <class G, class WF, class DistanceValue> // For exposition only
16+
concept edge_weight_function = // e.g. weight(uv)
17+
is_arithmetic_v<invoke_result_t<WF, edge_reference_t<G>>> &&
18+
basic_edge_weight_function<G, WF, DistanceValue, less<DistanceValue>, plus<DistanceValue>>;
19+
20+
/**
21+
* @ingroup graph_algorithms
22+
* @brief Returns a value to define an invalid distance used to initialize distance values
23+
* in the distance range before one of the shorts paths functions.
24+
*
25+
* @tparam DistanceValue The type of the distance.
26+
*
27+
* @return A unique sentinal value to indicate that a value is invalid, or undefined.
28+
*/
29+
template <class DistanceValue>
30+
constexpr auto shortest_path_invalid_distance() {
31+
return numeric_limits<DistanceValue>::max();
32+
}
33+
34+
/**
35+
* @ingroup graph_algorithms
36+
* @brief Returns a distance value of zero.
37+
*
38+
* @tparam DistanceValue The type of the distance.
39+
*
40+
* @return A value of zero distance.
41+
*/
42+
template <class DistanceValue>
43+
constexpr auto shortest_path_zero() {
44+
return DistanceValue();
45+
}
46+
47+
/**
48+
* @ingroup graph_algorithms
49+
* @brief Intializes the distance values to shortest_path_invalid_distance().
50+
*
51+
* @tparam Distances The range type of the distances.
52+
*
53+
* @param distances The range of distance values to initialize.
54+
*/
55+
template <class Distances>
56+
constexpr void init_shortest_paths(Distances& distances) {
57+
ranges::fill(distances, shortest_path_invalid_distance<ranges::range_value_t<Distances>>());
58+
}
59+
60+
/**
61+
* @ingroup graph_algorithms
62+
* @brief Intializes the distance and predecessor values for shortest paths algorithms.
63+
*
64+
* @tparam Distances The range type of the distances.
65+
* @tparam Predecessors The range type of the predecessors.
66+
*
67+
* @param distances The range of distance values to initialize.
68+
* @param predecessors The range of predecessors to initialize.
69+
*/
70+
template <class Distances, class Predecessors>
71+
constexpr void init_shortest_paths(Distances& distances, Predecessors& predecessors) {
72+
init_shortest_paths(distances);
73+
74+
using pred_t = ranges::range_value_t<Predecessors>;
75+
pred_t i = pred_t();
76+
for (auto& pred : predecessors)
77+
pred = i++;
78+
}
79+
80+
/**
81+
* @brief An always-empty random_access_range.
82+
*
83+
* A unique range type that can be used at compile time to determine if predecessors need to
84+
* be evaluated.
85+
*
86+
* This is not in the P1709 proposal. It's a quick hack to allow us to implement quickly.
87+
*/
88+
class _null_range_type : public std::vector<size_t> {
89+
using T = size_t;
90+
using Allocator = std::allocator<T>;
91+
using Base = std::vector<T, Allocator>;
92+
93+
public:
94+
_null_range_type() noexcept(noexcept(Allocator())) = default;
95+
explicit _null_range_type(const Allocator& alloc) noexcept {}
96+
_null_range_type(Base::size_type count, const T& value, const Allocator& alloc = Allocator()) {}
97+
explicit _null_range_type(Base::size_type count, const Allocator& alloc = Allocator()) {}
98+
template <class InputIt>
99+
_null_range_type(InputIt first, InputIt last, const Allocator& alloc = Allocator()) {}
100+
_null_range_type(const _null_range_type& other) : Base() {}
101+
_null_range_type(const _null_range_type& other, const Allocator& alloc) {}
102+
_null_range_type(_null_range_type&& other) noexcept {}
103+
_null_range_type(_null_range_type&& other, const Allocator& alloc) {}
104+
_null_range_type(std::initializer_list<T> init, const Allocator& alloc = Allocator()) {}
105+
};
106+
107+
inline static _null_range_type _null_predecessors;
108+
109+
} // namespace std::graph
110+
111+
#endif // GRAPH_COMMON_SHORTEST_PATHS_HPP

0 commit comments

Comments
 (0)