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