Google OR-Tools v9.14
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 OR_TOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
15#define OR_TOOLS_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 search_threads_.StartWorkers();
210}
211
212template <typename GraphType, typename DistanceType>
214 const Path& path) const {
215 if (path.meeting_point == -1) return "<NO PATH>";
216 std::string out;
217 for (const int arc : path.forward_arc_path) {
218 absl::StrAppend(&out, graph_[FORWARD]->Tail(arc), " --(#", arc, ":",
219 ((*arc_lengths_[FORWARD])[arc]), ")--> ");
220 }
221 absl::StrAppend(&out, "[", path.meeting_point, "]");
222 for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
223 absl::StrAppend(&out, " <--(#", arc, ":", ((*arc_lengths_[BACKWARD])[arc]),
224 ")-- ", graph_[BACKWARD]->Tail(arc));
225 }
226 return out;
227}
228
229template <typename GraphType, typename DistanceType>
230std::vector<typename GraphType::NodeIndex>
233 const {
234 if (path.meeting_point == -1) return {};
235 std::vector<int> nodes;
236 for (const int arc : path.forward_arc_path) {
237 nodes.push_back(graph_[FORWARD]->Tail(arc));
238 }
239 nodes.push_back(path.meeting_point);
240 for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
241 nodes.push_back(graph_[BACKWARD]->Tail(arc));
242 }
243 return nodes;
244}
245
246template <typename GraphType, typename DistanceType>
249 const std::vector<NodeDistance>& sources,
250 const std::vector<NodeDistance>& destinations)
251 // Disable thread safety analysis in this function, because there's no
252 // multi-threading within its body, per se: the multi-threading work
253 // is solely within PerformHalfSearch().
254 ABSL_NO_THREAD_SAFETY_ANALYSIS {
255 if (VLOG_IS_ON(2)) {
256 VLOG(2) << "Starting search with " << sources.size() << " sources and "
257 << destinations.size() << " destinations. Sources:";
258 for (const NodeDistance& src : sources) VLOG(2) << src.DebugString();
259 VLOG(2) << "Destinations:";
260 for (const NodeDistance& dst : destinations) VLOG(2) << dst.DebugString();
261 }
262 if (sources.empty() || destinations.empty()) return {-1, {}, {}};
263 // Initialize the fields that must be ready before both searches start.
264 for (const Direction dir : {FORWARD, BACKWARD}) {
265 const std::vector<NodeDistance>& srcs =
266 dir == FORWARD ? sources : destinations;
267 CHECK(queue_[dir].empty());
268 QCHECK_EQ(reached_nodes_[dir].size(), 0);
269 if (DEBUG_MODE) {
270 for (bool b : is_reached_[dir]) QCHECK(!b);
271 for (bool b : is_settled_[dir]) QCHECK(!b);
272 }
273 for (const NodeDistance& src : srcs) {
274 CHECK_GE(src.node, 0);
275 CHECK_LT(src.node, graph_[dir]->num_nodes());
276 is_source_[dir][src.node] = true;
277 if (!is_reached_[dir][src.node]) {
278 is_reached_[dir][src.node] = true;
279 reached_nodes_[dir].push_back(src.node);
280 parent_arc_[dir][src.node] = -1;
281 } else if (src.distance >= distances_[dir][src.node]) {
282 continue;
283 }
284 // If we're here, we have a new best distance for the current source.
285 // We also need to re-push it in the queue, since the distance changed.
286 distances_[dir][src.node] = src.distance;
287 queue_[dir].push(src);
288 }
289 }
290
291 // Start the Dijkstras!
292 best_meeting_point_ = {-1, infinity()};
293 absl::Notification search_has_ended[2];
294 search_threads_.Schedule([this, &search_has_ended, &sources]() {
295 PerformHalfSearch<FORWARD>(&search_has_ended[FORWARD]);
296 });
297 search_threads_.Schedule([this, &search_has_ended, &destinations]() {
298 PerformHalfSearch<BACKWARD>(&search_has_ended[BACKWARD]);
299 });
300
301 // Wait for the two searches to finish.
302 search_has_ended[FORWARD].WaitForNotification();
303 search_has_ended[BACKWARD].WaitForNotification();
304
305 // Clean up the rest of the search, sparsely. "is_settled" can't be cleaned
306 // in PerformHalfSearch() because it is needed by the other half-search
307 // (which might be still ongoing when the first half-search finishes), so
308 // we have to do it when both searches have ended.
309 // So we also clean the auxiliary field "reached_nodes" and the sibling field
310 // "is_reached" here too.
311 // Ditto for "is_source".
312 for (const Direction dir : {FORWARD, BACKWARD}) {
313 current_search_radius_[dir] = -infinity();
314 for (const int node : reached_nodes_[dir]) {
315 is_reached_[dir][node] = false;
316 is_settled_[dir][node] = false;
317 }
318 reached_nodes_[dir].clear();
319 }
320 for (const NodeDistance& src : sources) {
321 is_source_[FORWARD][src.node] = false;
322 }
323 for (const NodeDistance& dst : destinations) {
324 is_source_[BACKWARD][dst.node] = false;
325 }
326
327 // Extract the shortest path from the meeting point.
328 Path path = {best_meeting_point_.node, {}, {}};
329 if (path.meeting_point == -1) return path; // No path.
330
331 for (const Direction dir : {FORWARD, BACKWARD}) {
332 int node = path.meeting_point;
333 std::vector<int>* arc_path =
334 dir == FORWARD ? &path.forward_arc_path : &path.backward_arc_path;
335 while (true) {
336 const int arc = parent_arc_[dir][node];
337 if (arc < 0) break;
338 arc_path->push_back(arc);
339 node = graph_[dir]->Tail(arc);
340 }
341 std::reverse(arc_path->begin(), arc_path->end());
342 }
343 return path;
344}
345
346template <typename GraphType, typename DistanceType>
347template <
348 typename BidirectionalDijkstra<GraphType, DistanceType>::Direction dir>
349void BidirectionalDijkstra<GraphType, DistanceType>::PerformHalfSearch(
350 absl::Notification* search_has_ended) {
351 while (!queue_[dir].empty()) {
352 const NodeDistance top = queue_[dir].top();
353 queue_[dir].pop();
354
355 // The queue may contain the same node more than once, skip irrelevant
356 // entries.
357 if (is_settled_[dir][top.node]) continue;
358 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Popped "
359 << top.DebugString();
360
361 // Mark the node as settled. Since the "is_settled" might be read by the
362 // other search thread when updating the same node, we use a Mutex on that
363 // node.
364 {
365 node_mutex_[top.node].Lock();
366 is_settled_[dir][top.node] = true; // It's important to do this early.
367 // Most meeting points are caught by the logic below (in the arc
368 // relaxation loop), but not the meeting points that are on the sources
369 // or destinations. So we need this special case here.
370 if (is_source_[Reverse(dir)][top.node]) {
371 const DistanceType meeting_distance =
372 top.distance + distances_[Reverse(dir)][top.node];
373 // Release the node mutex, now that we can, to prevent deadlocks when
374 // we try acquiring the global search mutex.
375 node_mutex_[top.node].Unlock();
376 absl::MutexLock search_lock(&search_mutex_);
377 if (meeting_distance < best_meeting_point_.distance) {
378 best_meeting_point_ = {top.node, meeting_distance};
379 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
380 << ": New best: " << best_meeting_point_.DebugString();
381 }
382 } else {
383 node_mutex_[top.node].Unlock();
384 }
385 }
386
387 // Update the current search radius in this direction, and see whether we
388 // should stop the search, based on the other radius.
389 DistanceType potentially_interesting_distance_upper_bound;
390 {
391 absl::MutexLock lock(&search_mutex_);
392 current_search_radius_[dir] = top.distance;
393 potentially_interesting_distance_upper_bound =
394 best_meeting_point_.distance - current_search_radius_[Reverse(dir)];
395 }
396 if (top.distance >= potentially_interesting_distance_upper_bound) {
397 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Stopping.";
398 break;
399 }
400
401 // Visit the neighbors.
402 for (const int arc : graph_[dir]->OutgoingArcs(top.node)) {
403 const DistanceType candidate_distance =
404 top.distance + (*arc_lengths_[dir])[arc];
405 const int head = graph_[dir]->Head(arc);
406 if (!is_reached_[dir][head] ||
407 candidate_distance < distances_[dir][head]) {
408 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Pushing: "
409 << NodeDistance({head, candidate_distance}).DebugString();
410 if (!is_reached_[dir][head]) {
411 is_reached_[dir][head] = true;
412 reached_nodes_[dir].push_back(head);
413 }
414 parent_arc_[dir][head] = arc;
415
416 // SUBTLE: A simple performance optimization that speeds up the search
417 // (especially towards the end) is to avoid enqueuing nodes that can't
418 // possibly improve the current best meeting point.
419 // We still need to process them normally, though, including the
420 // meeting point logic below.
421 // TODO(user): Explain why.
422 if (candidate_distance < potentially_interesting_distance_upper_bound) {
423 queue_[dir].push({head, candidate_distance});
424 }
425
426 // Update the node distance and check for meeting points with the
427 // protection of a Mutex.
428 DistanceType meeting_distance = infinity();
429 {
430 absl::MutexLock node_lock(&node_mutex_[head]);
431 distances_[dir][head] = candidate_distance;
432 // Did we reach a meeting point?
433 if (is_settled_[Reverse(dir)][head]) {
434 meeting_distance =
435 candidate_distance + distances_[Reverse(dir)][head];
436 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
437 << ": Found meeting point!";
438 }
439 }
440 // Process the meeting point with the protection of the global search
441 // Mutex -- this is fine performance-wise because it happens rarely.
442 // To avoid deadlocks, we make sure that 'node_mutex' is no longer held.
443 if (meeting_distance != infinity()) {
444 absl::MutexLock search_lock(&search_mutex_);
445 if (meeting_distance < best_meeting_point_.distance) {
446 best_meeting_point_ = {head, meeting_distance};
447 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
448 << ": New best: " << best_meeting_point_.DebugString();
449 }
450 }
451 }
452 }
453 }
454 DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Done. Cleaning up...";
455
456 // Empty the queue.
457 while (!queue_[dir].empty()) queue_[dir].pop();
458
459 // We're done. Notify!
460 search_has_ended->Notify();
461}
462
463} // namespace operations_research
464#endif // OR_TOOLS_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)
In SWIG mode, we don't want anything besides these top-level includes.
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
NodeIndex meeting_point
The node where the two half-paths meet. -1 if no path exists.