Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
bidirectional_dijkstra.h
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#ifndef ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
15#define ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
16
17#include <algorithm>
18#include <limits>
19#include <queue>
20#include <string>
21#include <vector>
22
23#include "absl/base/thread_annotations.h"
24#include "absl/log/log.h"
25#include "absl/log/vlog_is_on.h"
26#include "absl/strings/str_cat.h"
27#include "absl/synchronization/mutex.h"
28#include "absl/synchronization/notification.h"
32
33namespace operations_research {
34
35// Run a bi-directional Dijkstra search, which can be much faster than
36// a typical Dijkstra, depending on the structure of the underlying graph.
37// It should be at least 2x faster when using 2 threads, but in practice
38// it can be much faster.
39// Eg. if the graph represents 3D points in the space and the distance is
40// the Euclidian distance, the search space grows like the cubic power of
41// the search radius, so the bi-directional Dijkstra can be expected to be
42// 2^3 = 8 times faster than the standard one.
43template <typename GraphType, typename DistanceType>
45 public:
46 typedef typename GraphType::NodeIndex NodeIndex;
47 typedef typename GraphType::ArcIndex ArcIndex;
48
49 // IMPORTANT: Both arguments must outlive the class. The arc lengths cannot be
50 // negative and the vector must be of the correct size (both preconditions are
51 // CHECKed).
52 // Two graphs are needed, for the forward and backward searches. Both graphs
53 // must have the same number of nodes.
54 // If you want to perform the search on a symmetric graph, you can simply
55 // provide it twice here, ditto for the arc lengths.
56 BidirectionalDijkstra(const GraphType* forward_graph,
57 const std::vector<DistanceType>* forward_arc_lengths,
58 const GraphType* backward_graph,
59 const std::vector<DistanceType>* backward_arc_lengths);
60
61 // Represents a node with a distance (typically from one end of the search,
62 // either the source or the destination).
63 struct NodeDistance {
65 DistanceType distance;
66 // We inverse the < operator to easily use this node within priority queues
67 // where the closest node comes first.
68 bool operator<(const NodeDistance& other) const {
69 return distance > other.distance;
70 }
71 std::string DebugString() const {
72 return absl::StrCat(node, ", d=", (distance));
73 }
74 };
75
76 // Represents a bidirectional path. See SetToSetShortestPath() to understand
77 // why this data structure is like this.
78 struct Path {
79 // The node where the two half-paths meet. -1 if no path exists.
81
82 // The forward arc path from a source to "meeting_point". Might be empty
83 // if no path is found, or if "meeting_point" is a source (the reverse
84 // implication doesn't work: even if meeting_point is a source Sa, there
85 // might be another source Sb != Sa such that the path [Sb....Sa] is shorter
86 // than [Sa], because of the initial distances).
87 std::vector<ArcIndex> forward_arc_path;
88
89 // Ditto, but those are arcs in the backwards graph, from a destination to
90 // the meeting point.
91 std::vector<ArcIndex> backward_arc_path;
92 };
93
94 // Returns a very nice debug string of the bidirectional path, eg:
95 // 0 --(#4:3.2)--> 1 --(#2:1.3)--> [5] <--(#8:5.6)-- 9 <--(#0:1.3)-- 3
96 // where the text in () is an arc's index followed by its length.
97 // Returns "<NO PATH>" for empty paths.
98 std::string PathDebugString(const Path& path) const;
99
100 // Converts the rich 'Path' structure into a simple node path, where
101 // the nodes go from the source to the destination (i.e. the backward
102 // path is reversed).
103 std::vector<NodeIndex> PathToNodePath(const Path& path) const;
104
105 // Finds the shortest path between two sets of nodes with costs, and returns
106 // a description of it as two half-paths of arcs (one in the forward graph,
107 // one in the backward graph) meeting at a "meeting point" node.
108 //
109 // When choosing the shortest path, the source and destination
110 // "initial distances" are taken into account: the overall path length is
111 // the sum of those and of the arc lengths. Note that this supports negative
112 // initial distances, as opposed to arc lengths which must be non-negative.
113 //
114 // Corner case: if a node is present several times in "sources" or in
115 // "destinations", only the entry with the smallest distance is taken into
116 // account.
117 Path SetToSetShortestPath(const std::vector<NodeDistance>& sources,
118 const std::vector<NodeDistance>& destinations);
119
120 // Shortcut for the common case when there is a single source and a single
121 // destination: in that case, source and destination cost don't matter.
123 return SetToSetShortestPath({{from, 0.0}}, {{to, 0.0}});
124 }
125
126 private:
127 enum Direction {
128 FORWARD = 0,
129 BACKWARD = 1,
130 };
131
132 inline static Direction Reverse(Direction d) {
133 return d == FORWARD ? BACKWARD : FORWARD;
134 }
135
136 inline static DistanceType infinity() {
137 return std::numeric_limits<DistanceType>::infinity();
138 }
139
140 template <Direction dir>
141 void PerformHalfSearch(absl::Notification* search_has_ended);
142
143 // Input forward/backward graphs with their arc lengths.
144 const GraphType* graph_[2];
145 const std::vector<DistanceType>* arc_lengths_[2];
146
147 // Priority queue of nodes, ordered by their distance to the source.
148 std::priority_queue<NodeDistance> queue_[2];
149
150 std::vector<bool> is_source_[2];
151 std::vector<bool> is_reached_[2];
152 // NOTE(user): is_settled is functionally vector<bool>, but we use
153 // vector<char> because the locking that it's involved in
154 // (via the per-node mutex, see below) works on entire memory bytes.
155 std::vector<char> is_settled_[2];
156
157 std::vector<DistanceType> distances_[2];
158 std::vector<ArcIndex> parent_arc_[2];
159 std::vector<NodeIndex> reached_nodes_[2];
160
161 // The per-node information shared by the two half searches are
162 // "is_settled_" and "distances_". Each direction exclusively writes in its
163 // own data, and reads the other direction's data.
164 // To avoid too much contention, we use a vector of one mutex per node.
165 //
166 // NOTE(user): There was no visible performance gain when using
167 // vector<bool> and, for example, one mutex for each group of 8 nodes
168 // spanning a byte (or for 64 nodes spanning 8 bytes).
169 //
170 // NOTE(user): There are arguments to simply remove the node Mutexes and
171 // the corresponding locks: the measured performance gain was 20%-30%, and
172 // the randomized correctness tests were passing (but TSAN was complaining).
173 // To be investigated if/when needed.
174 std::vector<absl::Mutex> node_mutex_;
175
176 absl::Mutex search_mutex_;
177 NodeDistance best_meeting_point_ ABSL_GUARDED_BY(search_mutex_);
178 DistanceType current_search_radius_[2] ABSL_GUARDED_BY(search_mutex_);
179
180 ThreadPool search_threads_;
181};
182
183template <typename GraphType, typename DistanceType>
185 const GraphType* forward_graph,
186 const std::vector<DistanceType>* forward_arc_lengths,
187 const GraphType* backward_graph,
188 const std::vector<DistanceType>* backward_arc_lengths)
189 : node_mutex_(forward_graph->num_nodes()), search_threads_(2) {
190 CHECK_EQ(forward_graph->num_nodes(), backward_graph->num_nodes());
191 const int num_nodes = forward_graph->num_nodes();
192 // Quickly verify that the arc lengths are non-negative.
193 for (int i = 0; i < num_nodes; ++i) {
194 CHECK_GE((*forward_arc_lengths)[i], 0) << i;
195 CHECK_GE((*backward_arc_lengths)[i], 0) << i;
196 }
197 graph_[FORWARD] = forward_graph;
198 graph_[BACKWARD] = backward_graph;
199 arc_lengths_[FORWARD] = forward_arc_lengths;
200 arc_lengths_[BACKWARD] = backward_arc_lengths;
201 for (const Direction dir : {FORWARD, BACKWARD}) {
202 current_search_radius_[dir] = -infinity();
203 is_source_[dir].assign(num_nodes, false);
204 is_reached_[dir].assign(num_nodes, false);
205 is_settled_[dir].assign(num_nodes, false);
206 distances_[dir].assign(num_nodes, infinity());
207 parent_arc_[dir].assign(num_nodes, -1);
208 }
209}
210
211template <typename GraphType, typename DistanceType>
213 const Path& path) const {
214 if (path.meeting_point == -1) return "<NO PATH>";
215 std::string out;
216 for (const int arc : path.forward_arc_path) {
217 absl::StrAppend(&out, graph_[FORWARD]->Tail(arc), " --(#", arc, ":",
218 ((*arc_lengths_[FORWARD])[arc]), ")--> ");
219 }
220 absl::StrAppend(&out, "[", path.meeting_point, "]");
221 for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
222 absl::StrAppend(&out, " <--(#", arc, ":", ((*arc_lengths_[BACKWARD])[arc]),
223 ")-- ", graph_[BACKWARD]->Tail(arc));
224 }
225 return out;
226}
227
228template <typename GraphType, typename DistanceType>
229std::vector<typename GraphType::NodeIndex>
232 const {
233 if (path.meeting_point == -1) return {};
234 std::vector<int> nodes;
235 for (const int arc : path.forward_arc_path) {
236 nodes.push_back(graph_[FORWARD]->Tail(arc));
237 }
238 nodes.push_back(path.meeting_point);
239 for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
240 nodes.push_back(graph_[BACKWARD]->Tail(arc));
241 }
242 return nodes;
243}
244
245template <typename GraphType, typename DistanceType>
248 const std::vector<NodeDistance>& sources,
249 const std::vector<NodeDistance>& destinations)
250 // Disable thread safety analysis in this function, because there's no
251 // multi-threading within its body, per se: the multi-threading work
252 // is solely within PerformHalfSearch().
253 ABSL_NO_THREAD_SAFETY_ANALYSIS {
254 if (VLOG_IS_ON(2)) {
255 VLOG(2) << "Starting search with " << sources.size() << " sources and "
256 << destinations.size() << " destinations. Sources:";
257 for (const NodeDistance& src : sources) VLOG(2) << src.DebugString();
258 VLOG(2) << "Destinations:";
259 for (const NodeDistance& dst : destinations) VLOG(2) << dst.DebugString();
260 }
261 if (sources.empty() || destinations.empty()) return {-1, {}, {}};
262 // Initialize the fields that must be ready before both searches start.
263 for (const Direction dir : {FORWARD, BACKWARD}) {
264 const std::vector<NodeDistance>& srcs =
265 dir == FORWARD ? sources : destinations;
266 CHECK(queue_[dir].empty());
267 QCHECK_EQ(reached_nodes_[dir].size(), 0);
268 if (DEBUG_MODE) {
269 for (bool b : is_reached_[dir]) QCHECK(!b);
270 for (bool b : is_settled_[dir]) QCHECK(!b);
271 }
272 for (const NodeDistance& src : srcs) {
273 CHECK_GE(src.node, 0);
274 CHECK_LT(src.node, graph_[dir]->num_nodes());
275 is_source_[dir][src.node] = true;
276 if (!is_reached_[dir][src.node]) {
277 is_reached_[dir][src.node] = true;
278 reached_nodes_[dir].push_back(src.node);
279 parent_arc_[dir][src.node] = -1;
280 } else if (src.distance >= distances_[dir][src.node]) {
281 continue;
282 }
283 // If we're here, we have a new best distance for the current source.
284 // We also need to re-push it in the queue, since the distance changed.
285 distances_[dir][src.node] = src.distance;
286 queue_[dir].push(src);
287 }
288 }
289
290 // Start the Dijkstras!
291 best_meeting_point_ = {-1, infinity()};
292 absl::Notification search_has_ended[2];
293 search_threads_.Schedule([this, &search_has_ended, &sources]() {
294 PerformHalfSearch<FORWARD>(&search_has_ended[FORWARD]);
295 });
296 search_threads_.Schedule([this, &search_has_ended, &destinations]() {
297 PerformHalfSearch<BACKWARD>(&search_has_ended[BACKWARD]);
298 });
299
300 // Wait for the two searches to finish.
301 search_has_ended[FORWARD].WaitForNotification();
302 search_has_ended[BACKWARD].WaitForNotification();
303
304 // Clean up the rest of the search, sparsely. "is_settled" can't be cleaned
305 // in PerformHalfSearch() because it is needed by the other half-search
306 // (which might be still ongoing when the first half-search finishes), so
307 // we have to do it when both searches have ended.
308 // So we also clean the auxiliary field "reached_nodes" and the sibling field
309 // "is_reached" here too.
310 // Ditto for "is_source".
311 for (const Direction dir : {FORWARD, BACKWARD}) {
312 current_search_radius_[dir] = -infinity();
313 for (const int node : reached_nodes_[dir]) {
314 is_reached_[dir][node] = false;
315 is_settled_[dir][node] = false;
316 }
317 reached_nodes_[dir].clear();
318 }
319 for (const NodeDistance& src : sources) {
320 is_source_[FORWARD][src.node] = false;
321 }
322 for (const NodeDistance& dst : destinations) {
323 is_source_[BACKWARD][dst.node] = false;
324 }
325
326 // Extract the shortest path from the meeting point.
327 Path path = {best_meeting_point_.node, {}, {}};
328 if (path.meeting_point == -1) return path; // No path.
329
330 for (const Direction dir : {FORWARD, BACKWARD}) {
331 int node = path.meeting_point;
332 std::vector<int>* arc_path =
333 dir == FORWARD ? &path.forward_arc_path : &path.backward_arc_path;
334 while (true) {
335 const int arc = parent_arc_[dir][node];
336 if (arc < 0) break;
337 arc_path->push_back(arc);
338 node = graph_[dir]->Tail(arc);
339 }
340 std::reverse(arc_path->begin(), arc_path->end());
341 }
342 return path;
343}
344
345template <typename GraphType, typename DistanceType>
346template <
347 typename BidirectionalDijkstra<GraphType, DistanceType>::Direction dir>
348void BidirectionalDijkstra<GraphType, DistanceType>::PerformHalfSearch(
349 absl::Notification* search_has_ended) {
350 while (!queue_[dir].empty()) {
351 const NodeDistance top = queue_[dir].top();
352 queue_[dir].pop();
353
354 // The queue may contain the same node more than once, skip irrelevant
355 // entries.
356 if (is_settled_[dir][top.node]) continue;
357 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Popped "
358 << top.DebugString();
359
360 // Mark the node as settled. Since the "is_settled" might be read by the
361 // other search thread when updating the same node, we use a Mutex on that
362 // node.
363 {
364 node_mutex_[top.node].Lock();
365 is_settled_[dir][top.node] = true; // It's important to do this early.
366 // Most meeting points are caught by the logic below (in the arc
367 // relaxation loop), but not the meeting points that are on the sources
368 // or destinations. So we need this special case here.
369 if (is_source_[Reverse(dir)][top.node]) {
370 const DistanceType meeting_distance =
371 top.distance + distances_[Reverse(dir)][top.node];
372 // Release the node mutex, now that we can, to prevent deadlocks when
373 // we try acquiring the global search mutex.
374 node_mutex_[top.node].Unlock();
375 absl::MutexLock search_lock(search_mutex_);
376 if (meeting_distance < best_meeting_point_.distance) {
377 best_meeting_point_ = {top.node, meeting_distance};
378 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
379 << ": New best: " << best_meeting_point_.DebugString();
380 }
381 } else {
382 node_mutex_[top.node].Unlock();
383 }
384 }
385
386 // Update the current search radius in this direction, and see whether we
387 // should stop the search, based on the other radius.
388 DistanceType potentially_interesting_distance_upper_bound;
389 {
390 absl::MutexLock lock(search_mutex_);
391 current_search_radius_[dir] = top.distance;
392 potentially_interesting_distance_upper_bound =
393 best_meeting_point_.distance - current_search_radius_[Reverse(dir)];
394 }
395 if (top.distance >= potentially_interesting_distance_upper_bound) {
396 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Stopping.";
397 break;
398 }
399
400 // Visit the neighbors.
401 for (const int arc : graph_[dir]->OutgoingArcs(top.node)) {
402 const DistanceType candidate_distance =
403 top.distance + (*arc_lengths_[dir])[arc];
404 const int head = graph_[dir]->Head(arc);
405 if (!is_reached_[dir][head] ||
406 candidate_distance < distances_[dir][head]) {
407 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Pushing: "
408 << NodeDistance({head, candidate_distance}).DebugString();
409 if (!is_reached_[dir][head]) {
410 is_reached_[dir][head] = true;
411 reached_nodes_[dir].push_back(head);
412 }
413 parent_arc_[dir][head] = arc;
414
415 // SUBTLE: A simple performance optimization that speeds up the search
416 // (especially towards the end) is to avoid enqueuing nodes that can't
417 // possibly improve the current best meeting point.
418 // We still need to process them normally, though, including the
419 // meeting point logic below.
420 // TODO(user): Explain why.
421 if (candidate_distance < potentially_interesting_distance_upper_bound) {
422 queue_[dir].push({head, candidate_distance});
423 }
424
425 // Update the node distance and check for meeting points with the
426 // protection of a Mutex.
427 DistanceType meeting_distance = infinity();
428 {
429 absl::MutexLock node_lock(node_mutex_[head]);
430 distances_[dir][head] = candidate_distance;
431 // Did we reach a meeting point?
432 if (is_settled_[Reverse(dir)][head]) {
433 meeting_distance =
434 candidate_distance + distances_[Reverse(dir)][head];
435 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
436 << ": Found meeting point!";
437 }
438 }
439 // Process the meeting point with the protection of the global search
440 // Mutex -- this is fine performance-wise because it happens rarely.
441 // To avoid deadlocks, we make sure that 'node_mutex' is no longer held.
442 if (meeting_distance != infinity()) {
443 absl::MutexLock search_lock(search_mutex_);
444 if (meeting_distance < best_meeting_point_.distance) {
445 best_meeting_point_ = {head, meeting_distance};
446 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
447 << ": New best: " << best_meeting_point_.DebugString();
448 }
449 }
450 }
451 }
452 }
453 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Done. Cleaning up...";
454
455 // Empty the queue.
456 while (!queue_[dir].empty()) queue_[dir].pop();
457
458 // We're done. Notify!
459 search_has_ended->Notify();
460}
461
462} // namespace operations_research
463#endif // ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
std::string PathDebugString(const Path &path) const
Path SetToSetShortestPath(const std::vector< NodeDistance > &sources, const std::vector< NodeDistance > &destinations)
std::vector< NodeIndex > PathToNodePath(const Path &path) const
Path OneToOneShortestPath(NodeIndex from, NodeIndex to)
BidirectionalDijkstra(const GraphType *forward_graph, const std::vector< DistanceType > *forward_arc_lengths, const GraphType *backward_graph, const std::vector< DistanceType > *backward_arc_lengths)
ReverseView< Container > reversed_view(const Container &c)
OR-Tools root namespace.
const bool DEBUG_MODE
Definition radix_sort.h:266
BeginEndReverseIteratorWrapper< Container > Reverse(const Container &c)
Definition iterators.h:115
trees with all degrees equal to