Files
ortools-clone/ortools/graph/bidirectional_dijkstra.h
Corentin Le Molgat b4b226801b update include guards
2025-11-05 11:54:02 +01:00

464 lines
19 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
#define ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_
#include <algorithm>
#include <limits>
#include <queue>
#include <string>
#include <vector>
#include "absl/base/thread_annotations.h"
#include "absl/log/log.h"
#include "absl/log/vlog_is_on.h"
#include "absl/strings/str_cat.h"
#include "absl/synchronization/mutex.h"
#include "absl/synchronization/notification.h"
#include "ortools/base/iterator_adaptors.h"
#include "ortools/base/logging.h"
#include "ortools/base/threadpool.h"
namespace operations_research {
// Run a bi-directional Dijkstra search, which can be much faster than
// a typical Dijkstra, depending on the structure of the underlying graph.
// It should be at least 2x faster when using 2 threads, but in practice
// it can be much faster.
// Eg. if the graph represents 3D points in the space and the distance is
// the Euclidian distance, the search space grows like the cubic power of
// the search radius, so the bi-directional Dijkstra can be expected to be
// 2^3 = 8 times faster than the standard one.
template <typename GraphType, typename DistanceType>
class BidirectionalDijkstra {
public:
typedef typename GraphType::NodeIndex NodeIndex;
typedef typename GraphType::ArcIndex ArcIndex;
// IMPORTANT: Both arguments must outlive the class. The arc lengths cannot be
// negative and the vector must be of the correct size (both preconditions are
// CHECKed).
// Two graphs are needed, for the forward and backward searches. Both graphs
// must have the same number of nodes.
// If you want to perform the search on a symmetric graph, you can simply
// provide it twice here, ditto for the arc lengths.
BidirectionalDijkstra(const GraphType* forward_graph,
const std::vector<DistanceType>* forward_arc_lengths,
const GraphType* backward_graph,
const std::vector<DistanceType>* backward_arc_lengths);
// Represents a node with a distance (typically from one end of the search,
// either the source or the destination).
struct NodeDistance {
NodeIndex node;
DistanceType distance;
// We inverse the < operator to easily use this node within priority queues
// where the closest node comes first.
bool operator<(const NodeDistance& other) const {
return distance > other.distance;
}
std::string DebugString() const {
return absl::StrCat(node, ", d=", (distance));
}
};
// Represents a bidirectional path. See SetToSetShortestPath() to understand
// why this data structure is like this.
struct Path {
// The node where the two half-paths meet. -1 if no path exists.
NodeIndex meeting_point;
// The forward arc path from a source to "meeting_point". Might be empty
// if no path is found, or if "meeting_point" is a source (the reverse
// implication doesn't work: even if meeting_point is a source Sa, there
// might be another source Sb != Sa such that the path [Sb....Sa] is shorter
// than [Sa], because of the initial distances).
std::vector<ArcIndex> forward_arc_path;
// Ditto, but those are arcs in the backwards graph, from a destination to
// the meeting point.
std::vector<ArcIndex> backward_arc_path;
};
// Returns a very nice debug string of the bidirectional path, eg:
// 0 --(#4:3.2)--> 1 --(#2:1.3)--> [5] <--(#8:5.6)-- 9 <--(#0:1.3)-- 3
// where the text in () is an arc's index followed by its length.
// Returns "<NO PATH>" for empty paths.
std::string PathDebugString(const Path& path) const;
// Converts the rich 'Path' structure into a simple node path, where
// the nodes go from the source to the destination (i.e. the backward
// path is reversed).
std::vector<NodeIndex> PathToNodePath(const Path& path) const;
// Finds the shortest path between two sets of nodes with costs, and returns
// a description of it as two half-paths of arcs (one in the forward graph,
// one in the backward graph) meeting at a "meeting point" node.
//
// When choosing the shortest path, the source and destination
// "initial distances" are taken into account: the overall path length is
// the sum of those and of the arc lengths. Note that this supports negative
// initial distances, as opposed to arc lengths which must be non-negative.
//
// Corner case: if a node is present several times in "sources" or in
// "destinations", only the entry with the smallest distance is taken into
// account.
Path SetToSetShortestPath(const std::vector<NodeDistance>& sources,
const std::vector<NodeDistance>& destinations);
// Shortcut for the common case when there is a single source and a single
// destination: in that case, source and destination cost don't matter.
Path OneToOneShortestPath(NodeIndex from, NodeIndex to) {
return SetToSetShortestPath({{from, 0.0}}, {{to, 0.0}});
}
private:
enum Direction {
FORWARD = 0,
BACKWARD = 1,
};
inline static Direction Reverse(Direction d) {
return d == FORWARD ? BACKWARD : FORWARD;
}
inline static DistanceType infinity() {
return std::numeric_limits<DistanceType>::infinity();
}
template <Direction dir>
void PerformHalfSearch(absl::Notification* search_has_ended);
// Input forward/backward graphs with their arc lengths.
const GraphType* graph_[2];
const std::vector<DistanceType>* arc_lengths_[2];
// Priority queue of nodes, ordered by their distance to the source.
std::priority_queue<NodeDistance> queue_[2];
std::vector<bool> is_source_[2];
std::vector<bool> is_reached_[2];
// NOTE(user): is_settled is functionally vector<bool>, but we use
// vector<char> because the locking that it's involved in
// (via the per-node mutex, see below) works on entire memory bytes.
std::vector<char> is_settled_[2];
std::vector<DistanceType> distances_[2];
std::vector<ArcIndex> parent_arc_[2];
std::vector<NodeIndex> reached_nodes_[2];
// The per-node information shared by the two half searches are
// "is_settled_" and "distances_". Each direction exclusively writes in its
// own data, and reads the other direction's data.
// To avoid too much contention, we use a vector of one mutex per node.
//
// NOTE(user): There was no visible performance gain when using
// vector<bool> and, for example, one mutex for each group of 8 nodes
// spanning a byte (or for 64 nodes spanning 8 bytes).
//
// NOTE(user): There are arguments to simply remove the node Mutexes and
// the corresponding locks: the measured performance gain was 20%-30%, and
// the randomized correctness tests were passing (but TSAN was complaining).
// To be investigated if/when needed.
std::vector<absl::Mutex> node_mutex_;
absl::Mutex search_mutex_;
NodeDistance best_meeting_point_ ABSL_GUARDED_BY(search_mutex_);
DistanceType current_search_radius_[2] ABSL_GUARDED_BY(search_mutex_);
ThreadPool search_threads_;
};
template <typename GraphType, typename DistanceType>
BidirectionalDijkstra<GraphType, DistanceType>::BidirectionalDijkstra(
const GraphType* forward_graph,
const std::vector<DistanceType>* forward_arc_lengths,
const GraphType* backward_graph,
const std::vector<DistanceType>* backward_arc_lengths)
: node_mutex_(forward_graph->num_nodes()), search_threads_(2) {
CHECK_EQ(forward_graph->num_nodes(), backward_graph->num_nodes());
const int num_nodes = forward_graph->num_nodes();
// Quickly verify that the arc lengths are non-negative.
for (int i = 0; i < num_nodes; ++i) {
CHECK_GE((*forward_arc_lengths)[i], 0) << i;
CHECK_GE((*backward_arc_lengths)[i], 0) << i;
}
graph_[FORWARD] = forward_graph;
graph_[BACKWARD] = backward_graph;
arc_lengths_[FORWARD] = forward_arc_lengths;
arc_lengths_[BACKWARD] = backward_arc_lengths;
for (const Direction dir : {FORWARD, BACKWARD}) {
current_search_radius_[dir] = -infinity();
is_source_[dir].assign(num_nodes, false);
is_reached_[dir].assign(num_nodes, false);
is_settled_[dir].assign(num_nodes, false);
distances_[dir].assign(num_nodes, infinity());
parent_arc_[dir].assign(num_nodes, -1);
}
}
template <typename GraphType, typename DistanceType>
std::string BidirectionalDijkstra<GraphType, DistanceType>::PathDebugString(
const Path& path) const {
if (path.meeting_point == -1) return "<NO PATH>";
std::string out;
for (const int arc : path.forward_arc_path) {
absl::StrAppend(&out, graph_[FORWARD]->Tail(arc), " --(#", arc, ":",
((*arc_lengths_[FORWARD])[arc]), ")--> ");
}
absl::StrAppend(&out, "[", path.meeting_point, "]");
for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
absl::StrAppend(&out, " <--(#", arc, ":", ((*arc_lengths_[BACKWARD])[arc]),
")-- ", graph_[BACKWARD]->Tail(arc));
}
return out;
}
template <typename GraphType, typename DistanceType>
std::vector<typename GraphType::NodeIndex>
BidirectionalDijkstra<GraphType, DistanceType>::PathToNodePath(
const typename BidirectionalDijkstra<GraphType, DistanceType>::Path& path)
const {
if (path.meeting_point == -1) return {};
std::vector<int> nodes;
for (const int arc : path.forward_arc_path) {
nodes.push_back(graph_[FORWARD]->Tail(arc));
}
nodes.push_back(path.meeting_point);
for (const int arc : gtl::reversed_view(path.backward_arc_path)) {
nodes.push_back(graph_[BACKWARD]->Tail(arc));
}
return nodes;
}
template <typename GraphType, typename DistanceType>
typename BidirectionalDijkstra<GraphType, DistanceType>::Path
BidirectionalDijkstra<GraphType, DistanceType>::SetToSetShortestPath(
const std::vector<NodeDistance>& sources,
const std::vector<NodeDistance>& destinations)
// Disable thread safety analysis in this function, because there's no
// multi-threading within its body, per se: the multi-threading work
// is solely within PerformHalfSearch().
ABSL_NO_THREAD_SAFETY_ANALYSIS {
if (VLOG_IS_ON(2)) {
VLOG(2) << "Starting search with " << sources.size() << " sources and "
<< destinations.size() << " destinations. Sources:";
for (const NodeDistance& src : sources) VLOG(2) << src.DebugString();
VLOG(2) << "Destinations:";
for (const NodeDistance& dst : destinations) VLOG(2) << dst.DebugString();
}
if (sources.empty() || destinations.empty()) return {-1, {}, {}};
// Initialize the fields that must be ready before both searches start.
for (const Direction dir : {FORWARD, BACKWARD}) {
const std::vector<NodeDistance>& srcs =
dir == FORWARD ? sources : destinations;
CHECK(queue_[dir].empty());
QCHECK_EQ(reached_nodes_[dir].size(), 0);
if (DEBUG_MODE) {
for (bool b : is_reached_[dir]) QCHECK(!b);
for (bool b : is_settled_[dir]) QCHECK(!b);
}
for (const NodeDistance& src : srcs) {
CHECK_GE(src.node, 0);
CHECK_LT(src.node, graph_[dir]->num_nodes());
is_source_[dir][src.node] = true;
if (!is_reached_[dir][src.node]) {
is_reached_[dir][src.node] = true;
reached_nodes_[dir].push_back(src.node);
parent_arc_[dir][src.node] = -1;
} else if (src.distance >= distances_[dir][src.node]) {
continue;
}
// If we're here, we have a new best distance for the current source.
// We also need to re-push it in the queue, since the distance changed.
distances_[dir][src.node] = src.distance;
queue_[dir].push(src);
}
}
// Start the Dijkstras!
best_meeting_point_ = {-1, infinity()};
absl::Notification search_has_ended[2];
search_threads_.Schedule([this, &search_has_ended, &sources]() {
PerformHalfSearch<FORWARD>(&search_has_ended[FORWARD]);
});
search_threads_.Schedule([this, &search_has_ended, &destinations]() {
PerformHalfSearch<BACKWARD>(&search_has_ended[BACKWARD]);
});
// Wait for the two searches to finish.
search_has_ended[FORWARD].WaitForNotification();
search_has_ended[BACKWARD].WaitForNotification();
// Clean up the rest of the search, sparsely. "is_settled" can't be cleaned
// in PerformHalfSearch() because it is needed by the other half-search
// (which might be still ongoing when the first half-search finishes), so
// we have to do it when both searches have ended.
// So we also clean the auxiliary field "reached_nodes" and the sibling field
// "is_reached" here too.
// Ditto for "is_source".
for (const Direction dir : {FORWARD, BACKWARD}) {
current_search_radius_[dir] = -infinity();
for (const int node : reached_nodes_[dir]) {
is_reached_[dir][node] = false;
is_settled_[dir][node] = false;
}
reached_nodes_[dir].clear();
}
for (const NodeDistance& src : sources) {
is_source_[FORWARD][src.node] = false;
}
for (const NodeDistance& dst : destinations) {
is_source_[BACKWARD][dst.node] = false;
}
// Extract the shortest path from the meeting point.
Path path = {best_meeting_point_.node, {}, {}};
if (path.meeting_point == -1) return path; // No path.
for (const Direction dir : {FORWARD, BACKWARD}) {
int node = path.meeting_point;
std::vector<int>* arc_path =
dir == FORWARD ? &path.forward_arc_path : &path.backward_arc_path;
while (true) {
const int arc = parent_arc_[dir][node];
if (arc < 0) break;
arc_path->push_back(arc);
node = graph_[dir]->Tail(arc);
}
std::reverse(arc_path->begin(), arc_path->end());
}
return path;
}
template <typename GraphType, typename DistanceType>
template <
typename BidirectionalDijkstra<GraphType, DistanceType>::Direction dir>
void BidirectionalDijkstra<GraphType, DistanceType>::PerformHalfSearch(
absl::Notification* search_has_ended) {
while (!queue_[dir].empty()) {
const NodeDistance top = queue_[dir].top();
queue_[dir].pop();
// The queue may contain the same node more than once, skip irrelevant
// entries.
if (is_settled_[dir][top.node]) continue;
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Popped "
<< top.DebugString();
// Mark the node as settled. Since the "is_settled" might be read by the
// other search thread when updating the same node, we use a Mutex on that
// node.
{
node_mutex_[top.node].Lock();
is_settled_[dir][top.node] = true; // It's important to do this early.
// Most meeting points are caught by the logic below (in the arc
// relaxation loop), but not the meeting points that are on the sources
// or destinations. So we need this special case here.
if (is_source_[Reverse(dir)][top.node]) {
const DistanceType meeting_distance =
top.distance + distances_[Reverse(dir)][top.node];
// Release the node mutex, now that we can, to prevent deadlocks when
// we try acquiring the global search mutex.
node_mutex_[top.node].Unlock();
absl::MutexLock search_lock(search_mutex_);
if (meeting_distance < best_meeting_point_.distance) {
best_meeting_point_ = {top.node, meeting_distance};
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
<< ": New best: " << best_meeting_point_.DebugString();
}
} else {
node_mutex_[top.node].Unlock();
}
}
// Update the current search radius in this direction, and see whether we
// should stop the search, based on the other radius.
DistanceType potentially_interesting_distance_upper_bound;
{
absl::MutexLock lock(search_mutex_);
current_search_radius_[dir] = top.distance;
potentially_interesting_distance_upper_bound =
best_meeting_point_.distance - current_search_radius_[Reverse(dir)];
}
if (top.distance >= potentially_interesting_distance_upper_bound) {
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Stopping.";
break;
}
// Visit the neighbors.
for (const int arc : graph_[dir]->OutgoingArcs(top.node)) {
const DistanceType candidate_distance =
top.distance + (*arc_lengths_[dir])[arc];
const int head = graph_[dir]->Head(arc);
if (!is_reached_[dir][head] ||
candidate_distance < distances_[dir][head]) {
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Pushing: "
<< NodeDistance({head, candidate_distance}).DebugString();
if (!is_reached_[dir][head]) {
is_reached_[dir][head] = true;
reached_nodes_[dir].push_back(head);
}
parent_arc_[dir][head] = arc;
// SUBTLE: A simple performance optimization that speeds up the search
// (especially towards the end) is to avoid enqueuing nodes that can't
// possibly improve the current best meeting point.
// We still need to process them normally, though, including the
// meeting point logic below.
// TODO(user): Explain why.
if (candidate_distance < potentially_interesting_distance_upper_bound) {
queue_[dir].push({head, candidate_distance});
}
// Update the node distance and check for meeting points with the
// protection of a Mutex.
DistanceType meeting_distance = infinity();
{
absl::MutexLock node_lock(node_mutex_[head]);
distances_[dir][head] = candidate_distance;
// Did we reach a meeting point?
if (is_settled_[Reverse(dir)][head]) {
meeting_distance =
candidate_distance + distances_[Reverse(dir)][head];
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
<< ": Found meeting point!";
}
}
// Process the meeting point with the protection of the global search
// Mutex -- this is fine performance-wise because it happens rarely.
// To avoid deadlocks, we make sure that 'node_mutex' is no longer held.
if (meeting_distance != infinity()) {
absl::MutexLock search_lock(search_mutex_);
if (meeting_distance < best_meeting_point_.distance) {
best_meeting_point_ = {head, meeting_distance};
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD")
<< ": New best: " << best_meeting_point_.DebugString();
}
}
}
}
}
DVLOG(2) << (dir ? "BACKWARD" : "FORWARD") << ": Done. Cleaning up...";
// Empty the queue.
while (!queue_[dir].empty()) queue_[dir].pop();
// We're done. Notify!
search_has_ended->Notify();
}
} // namespace operations_research
#endif // ORTOOLS_GRAPH_BIDIRECTIONAL_DIJKSTRA_H_