diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index bf5b28d80c..b2c004240c 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -376,10 +376,10 @@ cc_library( srcs = ["dag_shortest_path.cc"], hdrs = ["dag_shortest_path.h"], deps = [ + ":ebert_graph", ":graph", ":topologicalsorter", "@com_google_absl//absl/algorithm:container", - "@com_google_absl//absl/base:log_severity", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", diff --git a/ortools/graph/dag_constrained_shortest_path.h b/ortools/graph/dag_constrained_shortest_path.h index 92f0cbf9ff..441ddc32d3 100644 --- a/ortools/graph/dag_constrained_shortest_path.h +++ b/ortools/graph/dag_constrained_shortest_path.h @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -82,23 +81,6 @@ PathWithLength ConstrainedShortestPathsOnDag( // ----------------------------------------------------------------------------- // Advanced API. // ----------------------------------------------------------------------------- -#if __cplusplus >= 202002L -template -concept DagGraphType = requires(GraphType graph) { - { typename GraphType::NodeIndex{} }; - { typename GraphType::ArcIndex{} }; - { graph.num_nodes() } -> std::same_as; - { graph.num_arcs() } -> std::same_as; - { graph.OutgoingArcs(typename GraphType::NodeIndex{}) }; - { - graph.Tail(typename GraphType::ArcIndex{}) - } -> std::same_as; - { - graph.Head(typename GraphType::ArcIndex{}) - } -> std::same_as; -}; -#endif - // A wrapper that holds the memory needed to run many constrained shortest path // computations efficiently on the given DAG (on which resources do not change). // `GraphType` can use one of the interfaces defined in `util/graph/graph.h`. diff --git a/ortools/graph/dag_shortest_path.cc b/ortools/graph/dag_shortest_path.cc index bf4f2c3927..724e4aeee6 100644 --- a/ortools/graph/dag_shortest_path.cc +++ b/ortools/graph/dag_shortest_path.cc @@ -14,22 +14,32 @@ #include "ortools/graph/dag_shortest_path.h" #include +#include #include #include "absl/log/check.h" #include "absl/types/span.h" +#include "ortools/graph/ebert_graph.h" #include "ortools/graph/graph.h" #include "ortools/graph/topologicalsorter.h" namespace operations_research { -PathWithLength ShortestPathsOnDag( - const int num_nodes, absl::Span arcs_with_length, - int source, int destination) { - using GraphType = util::StaticGraph<>; - using NodeIndex = GraphType::NodeIndex; - using ArcIndex = GraphType::ArcIndex; +namespace { +using GraphType = util::StaticGraph<>; +using NodeIndex = GraphType::NodeIndex; +using ArcIndex = GraphType::ArcIndex; + +struct ShortestPathOnDagProblem { + GraphType graph; + std::vector arc_lengths; + std::vector original_arc_indices; + std::vector topological_order; +}; + +ShortestPathOnDagProblem ReadProblem( + const int num_nodes, absl::Span arcs_with_length) { GraphType graph(num_nodes, arcs_with_length.size()); std::vector arc_lengths; arc_lengths.reserve(arcs_with_length.size()); @@ -41,10 +51,10 @@ PathWithLength ShortestPathsOnDag( graph.Build(&permutation); util::Permute(permutation, &arc_lengths); - std::vector inverse_permutation(permutation.size()); + std::vector original_arc_indices(permutation.size()); if (!permutation.empty()) { for (ArcIndex i = 0; i < permutation.size(); ++i) { - inverse_permutation[permutation[i]] = i; + original_arc_indices[permutation[i]] = i; } } @@ -52,27 +62,75 @@ PathWithLength ShortestPathsOnDag( util::graph::FastTopologicalSort(graph); CHECK_OK(topological_order) << "arcs_with_length form a cycle."; - ShortestPathsOnDagWrapper> shortest_path_on_dag( - &graph, &arc_lengths, *topological_order); + return ShortestPathOnDagProblem{.graph = graph, + .arc_lengths = arc_lengths, + .original_arc_indices = original_arc_indices, + .topological_order = *topological_order}; +} +void GetOriginalArcPath(const std::vector& original_arc_indices, + std::vector& arc_path) { + if (original_arc_indices.empty()) { + return; + } + for (int i = 0; i < arc_path.size(); ++i) { + arc_path[i] = original_arc_indices[arc_path[i]]; + } +} + +} // namespace + +PathWithLength ShortestPathsOnDag( + const int num_nodes, absl::Span arcs_with_length, + const int source, const int destination) { + const ShortestPathOnDagProblem problem = + ReadProblem(num_nodes, arcs_with_length); + + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &problem.graph, &problem.arc_lengths, problem.topological_order); shortest_path_on_dag.RunShortestPathOnDag({source}); if (!shortest_path_on_dag.IsReachable(destination)) { - return PathWithLength{.length = std::numeric_limits::infinity(), - .arc_path = std::vector(), - .node_path = std::vector()}; + return PathWithLength{.length = std::numeric_limits::infinity()}; } std::vector arc_path = shortest_path_on_dag.ArcPathTo(destination); - if (!inverse_permutation.empty()) { - for (int i = 0; i < arc_path.size(); ++i) { - arc_path[i] = inverse_permutation[arc_path[i]]; - } - } + GetOriginalArcPath(problem.original_arc_indices, arc_path); return PathWithLength{ .length = shortest_path_on_dag.LengthTo(destination), .arc_path = arc_path, .node_path = shortest_path_on_dag.NodePathTo(destination)}; } +std::vector KShortestPathsOnDag( + const int num_nodes, absl::Span arcs_with_length, + const int source, const int destination, const int path_count) { + const ShortestPathOnDagProblem problem = + ReadProblem(num_nodes, arcs_with_length); + + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &problem.graph, &problem.arc_lengths, problem.topological_order, + path_count); + shortest_paths_on_dag.RunKShortestPathOnDag({source}); + + if (!shortest_paths_on_dag.IsReachable(destination)) { + return {PathWithLength{.length = std::numeric_limits::infinity()}}; + } + + std::vector lengths = shortest_paths_on_dag.LengthsTo(destination); + std::vector> arc_paths = + shortest_paths_on_dag.ArcPathsTo(destination); + std::vector> node_paths = + shortest_paths_on_dag.NodePathsTo(destination); + std::vector paths; + paths.reserve(lengths.size()); + for (int k = 0; k < lengths.size(); ++k) { + GetOriginalArcPath(problem.original_arc_indices, arc_paths[k]); + paths.push_back(PathWithLength{.length = lengths[k], + .arc_path = std::move(arc_paths[k]), + .node_path = std::move(node_paths[k])}); + } + return paths; +} + } // namespace operations_research diff --git a/ortools/graph/dag_shortest_path.h b/ortools/graph/dag_shortest_path.h index b8c3c645c4..08038870c6 100644 --- a/ortools/graph/dag_shortest_path.h +++ b/ortools/graph/dag_shortest_path.h @@ -15,11 +15,12 @@ #define OR_TOOLS_GRAPH_DAG_SHORTEST_PATH_H_ #include +#include +#include #include #include #include "absl/algorithm/container.h" -#include "absl/base/log_severity.h" #include "absl/log/check.h" #include "absl/status/status.h" #include "absl/strings/str_format.h" @@ -27,9 +28,8 @@ #include "ortools/base/logging.h" namespace operations_research { -// TODO(user): extend to non-floating lengths. -// TODO(user): extend to allow for length functor. -// TODO(user): extend to k-shortest paths for k > 1. +// TODO(b/332475231): extend to non-floating lengths. +// TODO(b/332476147): extend to allow for length functor. // This library provides a few APIs to compute the shortest path on a given // directed acyclic graph (DAG). @@ -63,23 +63,52 @@ struct PathWithLength { std::vector node_path; // includes the source node. }; -// Returns {+inf, {}} if there is no path of finite length from the source to -// the destination. Dies if `arcs_with_length` has a cycle. +// Returns {+inf, {}, {}} if there is no path of finite length from the source +// to the destination. Dies if `arcs_with_length` has a cycle. PathWithLength ShortestPathsOnDag( int num_nodes, absl::Span arcs_with_length, int source, int destination); +// Returns the k-shortest paths by increasing length. Returns fewer than k paths +// if there are fewer than k paths from the source to the destination. Returns +// {{+inf, {}, {}}} if there is no path of finite length from the source to the +// destination. Dies if `arcs_with_length` has a cycle. +std::vector KShortestPathsOnDag( + int num_nodes, absl::Span arcs_with_length, int source, + int destination, int path_count); + // ----------------------------------------------------------------------------- // Advanced API. // ----------------------------------------------------------------------------- +// This concept only enforces the standard graph API needed for all algorithms +// on DAGs. One could add the requirement of being a DAG wihtin this concept +// (which is done before running the algorithm). +#if __cplusplus >= 202002L +template +concept DagGraphType = requires(GraphType graph) { + { typename GraphType::NodeIndex{} }; + { typename GraphType::ArcIndex{} }; + { graph.num_nodes() } -> std::same_as; + { graph.num_arcs() } -> std::same_as; + { graph.OutgoingArcs(typename GraphType::NodeIndex{}) }; + { + graph.Tail(typename GraphType::ArcIndex{}) + } -> std::same_as; + { + graph.Head(typename GraphType::ArcIndex{}) + } -> std::same_as; + { graph.Build() }; +}; +#endif // A wrapper that holds the memory needed to run many shortest path computations -// efficiently on the given DAG. `GraphType` *must* implement the following -// types and functions: `GraphType::NodeIndex`, `GraphType::ArcIndex`, -// `num_nodes()`, `num_arcs()`, `OutgoingArcs(tail)`, `Tail(arc)` and -// `Head(arc)`. You can use on of the interfaces defined in -// `util/graph/graph.h`. +// efficiently on the given DAG. One call of `RunShortestPathOnDag()` has time +// complexity O(|E| + |V|) and space complexity O(|V|). +// `GraphType` can use any of the interfaces defined in `util/graph/graph.h`. template +#if __cplusplus >= 202002L + requires DagGraphType +#endif class ShortestPathsOnDagWrapper { public: using NodeIndex = typename GraphType::NodeIndex; @@ -116,9 +145,7 @@ class ShortestPathsOnDagWrapper { // Returns true if `node` is reachable from at least one source, i.e., the // length from at least one source is finite. - bool IsReachable(NodeIndex node) const { - return length_from_sources_[node] < std::numeric_limits::infinity(); - } + bool IsReachable(NodeIndex node) const; const std::vector& reached_nodes() const { return reached_nodes_; } // Returns the length of the shortest path from `node`'s source to `node`. @@ -138,6 +165,7 @@ class ShortestPathsOnDagWrapper { const std::vector& arc_lengths() const { return *arc_lengths_; } private: + static constexpr double kInf = std::numeric_limits::infinity(); const GraphType* const graph_; const std::vector* const arc_lengths_; absl::Span const topological_order_; @@ -148,122 +176,146 @@ class ShortestPathsOnDagWrapper { std::vector reached_nodes_; }; -template +// A wrapper that holds the memory needed to run many k-shortest paths +// computations efficiently on the given DAG. One call of +// `RunKShortestPathOnDag()` has time complexity O(|E| + k|V|log(d)) where d is +// the mean degree of the graph and space complexity O(k|V|). +// `GraphType` can use any of the interfaces defined in `util/graph/graph.h`. +// IMPORTANT: Only use if `path_count > 1` (k > 1) otherwise use +// `ShortestPathsOnDagWrapper`. +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +class KShortestPathsOnDagWrapper { + public: + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + + // IMPORTANT: All arguments must outlive the class. + // + // The vector of `arc_lengths` *must* be of size `graph.num_arcs()` and + // indexed the same way as in `graph`. + // + // You *must* provide a topological order. You can use + // `util::graph::FastTopologicalSort(graph)` to compute one if you don't + // already have one. An invalid topological order results in an upper bound + // for all shortest path computations. For maximum performance, you can + // further reindex the nodes under the topological order so that the memory + // access pattern is generally forward instead of random. For example, if the + // topological order for a graph with 4 nodes is [2,1,0,3], you can re-label + // the nodes 2, 1, and 0 to 0, 1, and 2 (and updates arcs accordingly). + // + // Validity of arcs and topological order are CHECKed if compiled in DEBUG + // mode. + // + // SUBTLE: You can modify the graph, the arc lengths or the topological order + // between calls to the `RunKShortestPathOnDag()` function. That's fine. Doing + // so will obviously invalidate the result API of the last shortest path run, + // which could return an upper bound, junk, or crash. + KShortestPathsOnDagWrapper(const GraphType* graph, + const std::vector* arc_lengths, + absl::Span topological_order, + int path_count); + + // Computes the shortest path to all reachable nodes from the given sources. + // This must be called before any of the query functions below. + void RunKShortestPathOnDag(absl::Span sources); + + // Returns true if `node` is reachable from at least one source, i.e., the + // length of the shortest path from at least one source is finite. + bool IsReachable(NodeIndex node) const; + const std::vector& reached_nodes() const { return reached_nodes_; } + + // Returns the lengths of the k-shortest paths from `node`'s source to `node` + // in increasing order. If there are less than k paths, return all path + // lengths. + std::vector LengthsTo(NodeIndex node) const; + + // Returns the list of all the arcs of the k-shortest paths from `node`'s + // source to `node`. + std::vector> ArcPathsTo(NodeIndex node) const; + + // Returns the list of all the nodes of the k-shortest paths from `node`'s + // source to `node` (including the source). CHECKs if the node is reachable. + std::vector> NodePathsTo(NodeIndex node) const; + + // Accessors to the underlying graph and arc lengths. + const GraphType& graph() const { return *graph_; } + const std::vector& arc_lengths() const { return *arc_lengths_; } + int path_count() const { return path_count_; } + + private: + static constexpr double kInf = std::numeric_limits::infinity(); + + const GraphType* const graph_; + const std::vector* const arc_lengths_; + absl::Span const topological_order_; + const int path_count_; + + GraphType reverse_graph_; + // Maps reverse arc indices to indices in the original graph. + std::vector arc_indices_; + + // Data about the last call of the `RunKShortestPathOnDag()` function. The + // first dimension is the index of the path (1st being the shortest). The + // second dimension are nodes. + std::vector> lengths_from_sources_; + std::vector> incoming_shortest_paths_arc_; + std::vector> incoming_shortest_paths_index_; + std::vector is_source_; + std::vector reached_nodes_; +}; + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif absl::Status TopologicalOrderIsValid( const GraphType& graph, absl::Span topological_order); // ----------------------------------------------------------------------------- -// Implementation. +// Implementations. // ----------------------------------------------------------------------------- - -template -ShortestPathsOnDagWrapper::ShortestPathsOnDagWrapper( - const GraphType* graph, const std::vector* arc_lengths, - absl::Span topological_order) - : graph_(graph), - arc_lengths_(arc_lengths), - topological_order_(topological_order) { - CHECK(graph_ != nullptr); - CHECK(arc_lengths_ != nullptr); - if (DEBUG_MODE) { - CHECK_EQ(arc_lengths_->size(), graph_->num_arcs()); - for (const double arc_length : *arc_lengths_) { - CHECK(arc_length != -std::numeric_limits::infinity() && - !std::isnan(arc_length)) - << absl::StrFormat("length cannot be -inf nor NaN"); - } - CHECK_OK(TopologicalOrderIsValid(*graph_, topological_order_)) - << "Invalid topological order"; - } - - // Memory allocation is done here and only once in order to avoid reallocation - // at each call of `RunShortestPathOnDag()` for better performance. - length_from_sources_.resize(graph_->num_nodes(), - std::numeric_limits::infinity()); - incoming_shortest_path_arc_.resize(graph_->num_nodes(), -1); - reached_nodes_.reserve(graph_->num_nodes()); -} - -template -void ShortestPathsOnDagWrapper::RunShortestPathOnDag( - absl::Span sources) { - // Caching the vector addresses allow to not fetch it on each access. - const absl::Span length_from_sources = - absl::MakeSpan(length_from_sources_); - const absl::Span arc_lengths = *arc_lengths_; - - // Note that we do not re-assign `incoming_shortest_path_arc_` at every call - // for better performance, so it only makes sense for nodes that are reachable - // from at least one source, the other ones will contain junk. - for (const NodeIndex node : reached_nodes_) { - length_from_sources[node] = std::numeric_limits::infinity(); - } - DCHECK(std::all_of( - length_from_sources.begin(), length_from_sources.end(), - [](double l) { return l == std::numeric_limits::infinity(); })); - reached_nodes_.clear(); - - for (const NodeIndex source : sources) { - length_from_sources[source] = 0.0; - } - - for (const NodeIndex tail : topological_order_) { - const double length_to_tail = length_from_sources[tail]; - // Stop exploring a node as soon as its length to all sources is +inf. - if (length_to_tail == std::numeric_limits::infinity()) { - continue; - } - reached_nodes_.push_back(tail); - for (const ArcIndex arc : graph_->OutgoingArcs(tail)) { - const NodeIndex head = graph_->Head(arc); - DCHECK(arc_lengths[arc] != -std::numeric_limits::infinity()); - const double length_to_head = arc_lengths[arc] + length_to_tail; - if (length_to_head < length_from_sources[head]) { - length_from_sources[head] = length_to_head; - incoming_shortest_path_arc_[head] = arc; - } - } - } -} - -// TODO(user): If `ArcPathTo` and/or `NodePathTo` functions become +// TODO(b/332475804): If `ArcPathTo` and/or `NodePathTo` functions become // bottlenecks: // (1) have the class preallocate a buffer of size `num_nodes` // (2) assign into an index rather than with push_back // (3) return by absl::Span (or return a copy) with known size. template -std::vector -ShortestPathsOnDagWrapper::ArcPathTo(NodeIndex node) const { - CHECK(IsReachable(node)); - std::vector arc_path; - ArcIndex current_arc = incoming_shortest_path_arc_[node]; - while (current_arc != -1) { - arc_path.push_back(current_arc); - current_arc = incoming_shortest_path_arc_[graph_->Tail(current_arc)]; +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector NodePathImpliedBy( + absl::Span arc_path, + const GraphType& graph) { + CHECK(!arc_path.empty()); + std::vector node_path; + node_path.reserve(arc_path.size() + 1); + for (const typename GraphType::ArcIndex arc_index : arc_path) { + node_path.push_back(graph.Tail(arc_index)); } - absl::c_reverse(arc_path); - return arc_path; -} - -template -std::vector -ShortestPathsOnDagWrapper::NodePathTo(NodeIndex node) const { - CHECK(IsReachable(node)); - std::vector node_path; - NodeIndex current_node = node; - node_path.push_back(current_node); - ArcIndex current_arc = incoming_shortest_path_arc_[current_node]; - while (current_arc != -1) { - current_node = graph_->Tail(current_arc); - node_path.push_back(current_node); - current_arc = incoming_shortest_path_arc_[current_node]; - } - absl::c_reverse(node_path); + node_path.push_back(graph.Head(arc_path.back())); return node_path; } -template +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +void CheckNodeIsValid(typename GraphType::NodeIndex node, + const GraphType& graph) { + CHECK_GE(node, 0) << "Node must be nonnegative. Input value: " << node; + CHECK_LT(node, graph.num_nodes()) + << "Node must be a valid node. Input value: " << node + << ". Number of nodes in the input graph: " << graph.num_nodes(); +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif absl::Status TopologicalOrderIsValid( const GraphType& graph, absl::Span topological_order) { @@ -296,5 +348,369 @@ absl::Status TopologicalOrderIsValid( return absl::OkStatus(); } +// ----------------------------------------------------------------------------- +// ShortestPathsOnDagWrapper implementation. +// ----------------------------------------------------------------------------- +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +ShortestPathsOnDagWrapper::ShortestPathsOnDagWrapper( + const GraphType* graph, const std::vector* arc_lengths, + absl::Span topological_order) + : graph_(graph), + arc_lengths_(arc_lengths), + topological_order_(topological_order) { + CHECK(graph_ != nullptr); + CHECK(arc_lengths_ != nullptr); + CHECK_GT(graph_->num_nodes(), 0) << "The graph is empty: it has no nodes"; + CHECK_GT(graph_->num_arcs(), 0) << "The graph is empty: it has no arcs"; +#ifndef NDEBUG + CHECK_EQ(arc_lengths_->size(), graph_->num_arcs()); + for (const double arc_length : *arc_lengths_) { + CHECK(arc_length != -kInf && !std::isnan(arc_length)) + << absl::StrFormat("length cannot be -inf nor NaN"); + } + CHECK_OK(TopologicalOrderIsValid(*graph_, topological_order_)) + << "Invalid topological order"; +#endif + + // Memory allocation is done here and only once in order to avoid reallocation + // at each call of `RunShortestPathOnDag()` for better performance. + length_from_sources_.resize(graph_->num_nodes(), kInf); + incoming_shortest_path_arc_.resize(graph_->num_nodes(), -1); + reached_nodes_.reserve(graph_->num_nodes()); +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +void ShortestPathsOnDagWrapper::RunShortestPathOnDag( + absl::Span sources) { + // Caching the vector addresses allow to not fetch it on each access. + const absl::Span length_from_sources = + absl::MakeSpan(length_from_sources_); + const absl::Span arc_lengths = *arc_lengths_; + + // Avoid reassigning `incoming_shortest_path_arc_` at every call for better + // performance, so it only makes sense for nodes that are reachable from at + // least one source, the other ones will contain junk. + for (const NodeIndex node : reached_nodes_) { + length_from_sources[node] = kInf; + } + DCHECK(std::all_of(length_from_sources.begin(), length_from_sources.end(), + [](double l) { return l == kInf; })); + reached_nodes_.clear(); + + for (const NodeIndex source : sources) { + CheckNodeIsValid(source, *graph_); + length_from_sources[source] = 0.0; + } + + for (const NodeIndex tail : topological_order_) { + const double length_to_tail = length_from_sources[tail]; + // Stop exploring a node as soon as its length to all sources is +inf. + if (length_to_tail == kInf) { + continue; + } + reached_nodes_.push_back(tail); + for (const ArcIndex arc : graph_->OutgoingArcs(tail)) { + const NodeIndex head = graph_->Head(arc); + DCHECK(arc_lengths[arc] != -kInf); + const double length_to_head = arc_lengths[arc] + length_to_tail; + if (length_to_head < length_from_sources[head]) { + length_from_sources[head] = length_to_head; + incoming_shortest_path_arc_[head] = arc; + } + } + } +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +bool ShortestPathsOnDagWrapper::IsReachable(NodeIndex node) const { + CheckNodeIsValid(node, *graph_); + return length_from_sources_[node] < kInf; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector +ShortestPathsOnDagWrapper::ArcPathTo(NodeIndex node) const { + CHECK(IsReachable(node)); + std::vector arc_path; + NodeIndex current_node = node; + for (int i = 0; i < graph_->num_nodes(); ++i) { + ArcIndex current_arc = incoming_shortest_path_arc_[current_node]; + if (current_arc == -1) { + break; + } + arc_path.push_back(current_arc); + current_node = graph_->Tail(current_arc); + } + absl::c_reverse(arc_path); + return arc_path; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector +ShortestPathsOnDagWrapper::NodePathTo(NodeIndex node) const { + const std::vector arc_path = ArcPathTo(node); + if (arc_path.empty()) { + return {node}; + } + return NodePathImpliedBy(ArcPathTo(node), *graph_); +} + +// ----------------------------------------------------------------------------- +// KShortestPathsOnDagWrapper implementation. +// ----------------------------------------------------------------------------- +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +KShortestPathsOnDagWrapper::KShortestPathsOnDagWrapper( + const GraphType* graph, const std::vector* arc_lengths, + absl::Span topological_order, const int path_count) + : graph_(graph), + arc_lengths_(arc_lengths), + topological_order_(topological_order), + path_count_(path_count) { + CHECK(graph_ != nullptr); + CHECK(arc_lengths_ != nullptr); + CHECK_GT(graph_->num_nodes(), 0) << "The graph is empty: it has no nodes"; + CHECK_GT(graph_->num_arcs(), 0) << "The graph is empty: it has no arcs"; + CHECK_GT(path_count_, 0) << "path_count must be greater than 0"; +#ifndef NDEBUG + CHECK_EQ(arc_lengths_->size(), graph_->num_arcs()); + for (const double arc_length : *arc_lengths_) { + CHECK(arc_length != -kInf && !std::isnan(arc_length)) + << absl::StrFormat("length cannot be -inf nor NaN"); + } + CHECK_OK(TopologicalOrderIsValid(*graph_, topological_order_)) + << "Invalid topological order"; +#endif + + // TODO(b/332475713): Optimize if reverse graph is already provided in + // `GraphType`. + const int num_arcs = graph_->num_arcs(); + reverse_graph_ = GraphType(graph_->num_nodes(), num_arcs); + for (ArcIndex arc_index = 0; arc_index < num_arcs; ++arc_index) { + reverse_graph_.AddArc(graph->Head(arc_index), graph->Tail(arc_index)); + } + std::vector permutation; + reverse_graph_.Build(&permutation); + arc_indices_.resize(permutation.size()); + if (!permutation.empty()) { + for (int i = 0; i < permutation.size(); ++i) { + arc_indices_[permutation[i]] = i; + } + } + + // Memory allocation is done here and only once in order to avoid reallocation + // at each call of `RunKShortestPathOnDag()` for better performance. + lengths_from_sources_.resize(path_count_); + incoming_shortest_paths_arc_.resize(path_count_); + incoming_shortest_paths_index_.resize(path_count_); + for (int k = 0; k < path_count_; ++k) { + lengths_from_sources_[k].resize(graph_->num_nodes(), kInf); + incoming_shortest_paths_arc_[k].resize(graph_->num_nodes(), -1); + incoming_shortest_paths_index_[k].resize(graph_->num_nodes(), -1); + } + is_source_.resize(graph_->num_nodes(), false); + reached_nodes_.reserve(graph_->num_nodes()); +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +void KShortestPathsOnDagWrapper::RunKShortestPathOnDag( + absl::Span sources) { + // Caching the vector addresses allow to not fetch it on each access. + const absl::Span arc_lengths = *arc_lengths_; + const absl::Span arc_indices = arc_indices_; + + // Avoid reassigning `incoming_shortest_path_arc_` at every call for better + // performance, so it only makes sense for nodes that are reachable from at + // least one source, the other ones will contain junk. + + for (const NodeIndex node : reached_nodes_) { + is_source_[node] = false; + for (int k = 0; k < path_count_; ++k) { + lengths_from_sources_[k][node] = kInf; + } + } + reached_nodes_.clear(); +#ifndef NDEBUG + for (int k = 0; k < path_count_; ++k) { + CHECK(std::all_of(lengths_from_sources_[k].begin(), + lengths_from_sources_[k].end(), + [](double l) { return l == kInf; })); + } +#endif + + for (const NodeIndex source : sources) { + CheckNodeIsValid(source, *graph_); + is_source_[source] = true; + } + + struct IncomingArcPath { + double path_length = 0.0; + ArcIndex arc_index = 0; + double arc_length = 0.0; + NodeIndex from = 0; + int path_index = 0; + + bool operator<(const IncomingArcPath& other) const { + return std::tie(path_length, from) < + std::tie(other.path_length, other.from); + } + bool operator>(const IncomingArcPath& other) const { return other < *this; } + }; + std::vector min_heap; + auto comp = std::greater(); + min_heap.reserve(path_count_ + 1); + for (const NodeIndex to : topological_order_) { + min_heap.clear(); + if (is_source_[to]) { + min_heap.push_back({.arc_index = -1}); + } + for (const ArcIndex reverse_arc_index : reverse_graph_.OutgoingArcs(to)) { + const ArcIndex arc_index = arc_indices.empty() + ? reverse_arc_index + : arc_indices[reverse_arc_index]; + const NodeIndex from = graph_->Tail(arc_index); + const double arc_length = arc_lengths[arc_index]; + DCHECK(arc_length != -kInf); + const double path_length = + lengths_from_sources_.front()[from] + arc_length; + if (path_length == kInf) { + continue; + } + min_heap.push_back({.path_length = path_length, + .arc_index = arc_index, + .arc_length = arc_length, + .from = from}); + std::push_heap(min_heap.begin(), min_heap.end(), comp); + if (min_heap.size() > path_count_) { + min_heap.pop_back(); + } + } + if (min_heap.empty()) { + continue; + } + reached_nodes_.push_back(to); + for (int k = 0; k < path_count_; ++k) { + std::pop_heap(min_heap.begin(), min_heap.end(), comp); + IncomingArcPath& incoming_arc_path = min_heap.back(); + lengths_from_sources_[k][to] = incoming_arc_path.path_length; + incoming_shortest_paths_arc_[k][to] = incoming_arc_path.arc_index; + incoming_shortest_paths_index_[k][to] = incoming_arc_path.path_index; + if (incoming_arc_path.arc_index != -1 && + incoming_arc_path.path_index < path_count_ - 1 && + lengths_from_sources_[incoming_arc_path.path_index + 1] + [incoming_arc_path.from] < kInf) { + ++incoming_arc_path.path_index; + incoming_arc_path.path_length = + lengths_from_sources_[incoming_arc_path.path_index] + [incoming_arc_path.from] + + incoming_arc_path.arc_length; + std::push_heap(min_heap.begin(), min_heap.end(), comp); + } else { + min_heap.pop_back(); + if (min_heap.empty()) { + break; + } + } + } + } +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +bool KShortestPathsOnDagWrapper::IsReachable(NodeIndex node) const { + CheckNodeIsValid(node, *graph_); + return lengths_from_sources_.front()[node] < kInf; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector KShortestPathsOnDagWrapper::LengthsTo( + NodeIndex node) const { + std::vector lengths_to; + lengths_to.reserve(path_count_); + for (int k = 0; k < path_count_; ++k) { + const double length_to = lengths_from_sources_[k][node]; + if (length_to == kInf) { + break; + } + lengths_to.push_back(length_to); + } + return lengths_to; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector> +KShortestPathsOnDagWrapper::ArcPathsTo(NodeIndex node) const { + std::vector> arc_paths; + arc_paths.reserve(path_count_); + for (int k = 0; k < path_count_; ++k) { + if (lengths_from_sources_[k][node] == kInf) { + break; + } + std::vector arc_path; + int current_path_index = k; + NodeIndex current_node = node; + for (int i = 0; i < graph_->num_nodes(); ++i) { + ArcIndex current_arc = + incoming_shortest_paths_arc_[current_path_index][current_node]; + if (current_arc == -1) { + break; + } + arc_path.push_back(current_arc); + current_path_index = + incoming_shortest_paths_index_[current_path_index][current_node]; + current_node = graph_->Tail(current_arc); + } + absl::c_reverse(arc_path); + arc_paths.push_back(arc_path); + } + return arc_paths; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector> +KShortestPathsOnDagWrapper::NodePathsTo(NodeIndex node) const { + const std::vector> arc_paths = ArcPathsTo(node); + std::vector> node_paths(arc_paths.size()); + for (int k = 0; k < arc_paths.size(); ++k) { + if (arc_paths[k].empty()) { + node_paths[k] = {node}; + } else { + node_paths[k] = NodePathImpliedBy(arc_paths[k], *graph_); + } + } + return node_paths; +} + } // namespace operations_research #endif // OR_TOOLS_GRAPH_DAG_SHORTEST_PATH_H_ diff --git a/ortools/graph/dag_shortest_path_test.cc b/ortools/graph/dag_shortest_path_test.cc index 677ea402cf..a4f7a0c1a1 100644 --- a/ortools/graph/dag_shortest_path_test.cc +++ b/ortools/graph/dag_shortest_path_test.cc @@ -44,6 +44,82 @@ using ::testing::HasSubstr; using ::testing::IsEmpty; using ::testing::status::StatusIs; +TEST(TopologicalOrderIsValidTest, ValidateTopologicalOrder) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/2); + graph.AddArc(source, destination); + + EXPECT_OK(TopologicalOrderIsValid(graph, {source, destination})); + EXPECT_THAT(TopologicalOrderIsValid(graph, {source}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("topological_order.size() = 1"))); + EXPECT_THAT(TopologicalOrderIsValid(graph, {source, source}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("0 appears twice"))); + EXPECT_THAT(TopologicalOrderIsValid(graph, {destination, source}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("arc (0, 1) is inconsistent"))); + + graph.AddArc(source, source); + + EXPECT_THAT(TopologicalOrderIsValid(graph, {source, destination}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("arc (0, 0) is inconsistent"))); +} + +// ----------------------------------------------------------------------------- +// ShortestPathOnDagTest and ShortestPathsOnDagWrapperTest. +// ----------------------------------------------------------------------------- +TEST(ShortestPathOnDagTest, EmptyGraph) { + EXPECT_DEATH(ShortestPathsOnDag(/*num_nodes=*/0, /*arcs_with_length=*/{}, + /*source=*/0, /*destination=*/0), + "num_nodes\\(\\) > 0"); +} + +TEST(ShortestPathOnDagTest, NoArcGraph) { + EXPECT_DEATH(ShortestPathsOnDag(/*num_nodes=*/1, /*arcs_with_length=*/{}, + /*source=*/0, /*destination=*/0), + "num_arcs\\(\\) > 0"); +} + +TEST(ShortestPathOnDagTest, NonExistingSourceBecauseNegative) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/-1, /*destination=*/1), + "Node must be nonnegative"); +} + +TEST(ShortestPathOnDagTest, NonExistingSourceBecauseTooLarge) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/3, /*destination=*/1), + "num_nodes\\(\\)"); +} + +TEST(ShortestPathOnDagTest, NonExistingDestinationBecauseNegative) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/0, /*destination=*/-1), + "Node must be nonnegative"); +} + +TEST(ShortestPathOnDagTest, NonExistingDestinationBecauseTooLarge) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/0, /*destination=*/3), + "num_nodes\\(\\)"); +} + +TEST(ShortestPathOnDagTest, Cycle) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, + /*arcs_with_length=*/{{0, 1, 0.0}, {1, 0, 0.0}}, + /*source=*/0, /*destination=*/1), + "cycle"); +} + TEST(ShortestPathOnDagTest, SimpleGraph) { const int source = 0; const int destination = 1; @@ -61,6 +137,19 @@ TEST(ShortestPathOnDagTest, SimpleGraph) { /*node_path=*/ElementsAre(source, a, destination))); } +TEST(ShortestPathOnDagTest, SourceIsDestination) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length = { + {source, destination, 1.0}}; + + EXPECT_THAT(ShortestPathsOnDag(num_nodes, arcs_with_length, source, source), + FieldsAre( + /*length=*/0.0, /*arc_path=*/IsEmpty(), + /*node_path=*/ElementsAre(source))); +} + TEST(ShortestPathOnDagTest, LargerGraphWithNegativeCost) { const int source = 0; const int a = 3; @@ -78,42 +167,6 @@ TEST(ShortestPathOnDagTest, LargerGraphWithNegativeCost) { /*node_path=*/ElementsAre(source, a, b, destination))); } -TEST(ShortestPathOnDagTest, Cycle) { - const int source = 0; - const int destination = 1; - const int num_nodes = 2; - const std::vector arcs_with_length = { - {source, destination, 1.0}, {destination, source, 2.0}}; - - EXPECT_DEATH( - ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), - "cycle"); -} - -TEST(ShortestPathOnDagTest, NaNWeight) { - const int source = 0; - const int destination = 1; - const int num_nodes = 2; - const std::vector arcs_with_length = { - {source, destination, std::numeric_limits::quiet_NaN()}}; - - EXPECT_DEBUG_DEATH( - ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), - "NaN"); -} - -TEST(ShortestPathOnDagTest, MinusInfWeight) { - const int source = 0; - const int destination = 1; - const int num_nodes = 2; - const std::vector arcs_with_length = { - {source, destination, -kInf}}; - - EXPECT_DEBUG_DEATH( - ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), - "-inf"); -} - TEST(ShortestPathOnDagTest, SetsNotConnected) { const int source = 0; const int destination = 1; @@ -127,6 +180,26 @@ TEST(ShortestPathOnDagTest, SetsNotConnected) { /*node_path=*/IsEmpty())); } +TEST(ShortestPathOnDagTest, TwoConnectedComponents) { + const int a = 0; + const int b = 1; + const int c = 2; + const int d = 3; + const int e = 4; + const int num_nodes = 5; + const std::vector arcs_with_length = { + {a, b, 0.0}, {b, c, 0.0}, {d, e, 0.0}}; + + EXPECT_THAT(ShortestPathsOnDag(num_nodes, arcs_with_length, /*source=*/a, + /*destination=*/e), + FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); + EXPECT_THAT(ShortestPathsOnDag(num_nodes, arcs_with_length, /*source=*/b, + /*destination=*/d), + FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); +} + TEST(ShortestPathOnDagTest, SetsNotConnectedDueToInfiniteCost) { const int source = 0; const int destination = 1; @@ -209,21 +282,6 @@ TEST(ShortestPathOnDagTest, UpdateCost) { /*node_path=*/ElementsAre(source, b, destination))); } -TEST(ShortestPathsOnDagWrapperTest, ValidateTopologicalOrder) { - const int source = 0; - const int destination = 1; - const int num_nodes = 2; - util::ListGraph<> graph(num_nodes, /*arc_capacity=*/1); - std::vector arc_lengths; - graph.AddArc(source, destination); - arc_lengths.push_back(1.0); - const std::vector topological_order = {source}; - - EXPECT_DEBUG_DEATH(ShortestPathsOnDagWrapper>( - &graph, &arc_lengths, topological_order), - "Invalid topological order"); -} - TEST(ShortestPathsOnDagWrapperTest, MultipleSources) { const int source_1 = 0; const int source_2 = 1; @@ -247,7 +305,7 @@ TEST(ShortestPathsOnDagWrapperTest, MultipleSources) { ElementsAre(source_1, destination)); } -TEST(ShortestPathOnDagTest, ShortestPathGoesThroughMultipleSources) { +TEST(ShortestPathsOnDagWrapperTest, ShortestPathGoesThroughMultipleSources) { const int source_1 = 0; const int source_2 = 1; const int destination = 2; @@ -270,7 +328,7 @@ TEST(ShortestPathOnDagTest, ShortestPathGoesThroughMultipleSources) { ElementsAre(source_1, source_2, destination)); } -TEST(ShortestPathOnDagTest, MultipleDestinations) { +TEST(ShortestPathsOnDagWrapperTest, MultipleDestinations) { const int source = 0; const int destination_1 = 1; const int destination_2 = 2; @@ -347,31 +405,6 @@ TEST(ShortestPathsOnDagWrapperTest, UpdateCost) { ElementsAre(source, b, destination)); } -TEST(TopologicalOrderIsValidTest, ValidateTopologicalOrder) { - const int source = 0; - const int destination = 1; - const int num_nodes = 2; - util::ListGraph<> graph(num_nodes, /*arc_capacity=*/2); - graph.AddArc(source, destination); - - EXPECT_OK(TopologicalOrderIsValid(graph, {source, destination})); - EXPECT_THAT(TopologicalOrderIsValid(graph, {source}), - StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("topological_order.size() = 1"))); - EXPECT_THAT(TopologicalOrderIsValid(graph, {source, source}), - StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("0 appears twice"))); - EXPECT_THAT(TopologicalOrderIsValid(graph, {destination, source}), - StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("arc (0, 1) is inconsistent"))); - - graph.AddArc(source, source); - - EXPECT_THAT(TopologicalOrderIsValid(graph, {source, destination}), - StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("arc (0, 0) is inconsistent"))); -} - // Builds a random DAG with a given number of nodes and arcs where 0 is always // the first and num_nodes-1 the last element in the topological order. Note // that the graph always include at least one arc from 0 to num_nodes-1. @@ -424,7 +457,7 @@ std::vector GenerateRandomLengths(const util::StaticGraph<>& graph, return arc_lengths; } -TEST(ShortestPathOnDagTest, RandomizedStressTest) { +TEST(ShortestPathsOnDagWrapperTest, RandomizedStressTest) { absl::BitGen bit_gen; const int kNumTests = 10000; for (int test = 0; test < kNumTests; ++test) { @@ -492,8 +525,41 @@ TEST(ShortestPathOnDagTest, RandomizedStressTest) { } } +// Debug tests. +#ifndef NDEBUG +TEST(ShortestPathOnDagTest, MinusInfWeight) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, -kInf}}, + /*source=*/0, /*destination=*/1), + "-inf"); +} + +TEST(ShortestPathOnDagTest, NaNWeight) { + EXPECT_DEATH( + ShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/ + {{0, 1, std::numeric_limits::quiet_NaN()}}, + /*source=*/0, /*destination=*/1), + "NaN"); +} + +TEST(ShortestPathsOnDagWrapperTest, ValidateTopologicalOrder) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/1); + std::vector arc_lengths; + graph.AddArc(source, destination); + arc_lengths.push_back(1.0); + const std::vector topological_order = {source}; + + EXPECT_DEATH(ShortestPathsOnDagWrapper>( + &graph, &arc_lengths, topological_order), + "Invalid topological order"); +} +#endif // NDEBUG + // ----------------------------------------------------------------------------- -// Benchmark. +// ShortestPathsOnDagWrapper benchmarks. // ----------------------------------------------------------------------------- void BM_RandomDag(benchmark::State& state) { absl::BitGen bit_gen; @@ -556,5 +622,517 @@ void BM_LineDag(benchmark::State& state) { BENCHMARK(BM_LineDag)->Arg(1 << 16)->Arg(1 << 22)->Arg(1 << 24); +// ----------------------------------------------------------------------------- +// KShortestPathOnDagTest and KShortestPathsOnDagWrapperTest. +// ----------------------------------------------------------------------------- +TEST(KShortestPathOnDagTest, EmptyGraph) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/0, /*arcs_with_length=*/{}, + /*source=*/0, /*destination=*/0, /*path_count=*/2), + "num_nodes\\(\\) > 0"); +} + +TEST(KShortestPathOnDagTest, NoArcGraph) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/1, /*arcs_with_length=*/{}, + /*source=*/0, /*destination=*/0, /*path_count=*/2), + "num_arcs\\(\\) > 0"); +} + +TEST(KShortestPathOnDagTest, NonExistingSourceBecauseNegative) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/-1, /*destination=*/1, /*path_count=*/2), + "Node must be nonnegative"); +} + +TEST(KShortestPathOnDagTest, NonExistingSourceBecauseTooLarge) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/3, /*destination=*/1, /*path_count=*/2), + "num_nodes\\(\\)"); +} + +TEST(KShortestPathOnDagTest, NonExistingDestinationBecauseNegative) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/0, /*destination=*/-1, /*path_count=*/2), + "Node must be nonnegative"); +} + +TEST(KShortestPathOnDagTest, NonExistingDestinationBecauseTooLarge) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/0, /*destination=*/3, /*path_count=*/2), + "num_nodes\\(\\)"); +} + +TEST(KShortestPathOnDagTest, KEqualsZero) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, 0.0}}, + /*source=*/0, /*destination=*/1, /*path_count=*/0), + "path_count must be greater than 0"); +} + +TEST(KShortestPathOnDagTest, OnlyHasOnePath) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length = {{source, a, 1.0}, + {a, destination, 1.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/2.0, /*arc_path=*/ElementsAre(0, 1), + /*node_path=*/ElementsAre(source, a, destination)))); +} + +TEST(KShortestPathOnDagTest, SourceIsDestination) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length = { + {source, destination, 1.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, source, + /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/0.0, /*arc_path=*/IsEmpty(), + /*node_path=*/ElementsAre(source)))); +} + +TEST(KShortestPathOnDagTest, HasTwoPaths) { + const int source = 0; + const int a = 1; + const int destination = 2; + const int num_nodes = 3; + const std::vector arcs_with_length = { + {source, a, 1.0}, {source, destination, 30.0}, {a, destination, 1.0}}; + + EXPECT_THAT( + KShortestPathsOnDag(num_nodes, arcs_with_length, source, destination, + /*path_count=*/3), + ElementsAre(FieldsAre( + /*length=*/2.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination)), + FieldsAre( + /*length=*/30.0, /*arc_path=*/ElementsAre(1), + /*node_path=*/ElementsAre(source, destination)))); +} + +TEST(KShortestPathOnDagTest, HasTwoPathsWithLongerPath) { + const int source = 0; + const int a = 1; + const int b = 2; + const int c = 3; + const int destination = 4; + const int num_nodes = 5; + const std::vector arcs_with_length = { + {source, a, 1.0}, + {source, destination, 30.0}, + {a, b, 1.0}, + {b, c, 1.0}, + {c, destination, 1.0}}; + + EXPECT_THAT( + KShortestPathsOnDag(num_nodes, arcs_with_length, source, destination, + /*path_count=*/3), + ElementsAre(FieldsAre( + /*length=*/4.0, /*arc_path=*/ElementsAre(0, 2, 3, 4), + /*node_path=*/ElementsAre(source, a, b, c, destination)), + FieldsAre( + /*length=*/30.0, /*arc_path=*/ElementsAre(1), + /*node_path=*/ElementsAre(source, destination)))); +} + +TEST(KShortestPathOnDagTest, LargerGraphWithNegativeCost) { + const int source = 0; + const int a = 3; + const int b = 2; + const int c = 1; + const int destination = 4; + const int num_nodes = 5; + const std::vector arcs_with_length = { + {a, c, 5.0}, {source, b, 7.0}, {a, b, 1.0}, + {source, a, 3.0}, {c, destination, 5.0}, {b, destination, -2.0}}; + + EXPECT_THAT( + KShortestPathsOnDag(num_nodes, arcs_with_length, source, destination, + /*path_count=*/2), + ElementsAre( + FieldsAre(/*length=*/2.0, /*arc_path=*/ElementsAre(3, 2, 5), + /*node_path=*/ElementsAre(source, a, b, destination)), + FieldsAre(/*length=*/5.0, /*arc_path=*/ElementsAre(1, 5), + /*node_path=*/ElementsAre(source, b, destination)))); +} + +TEST(KShortestPathOnDagTest, SetsNotConnected) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length = {{source, a, 1.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/2), + ElementsAre(FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty()))); +} + +TEST(KShortestPathOnDagTest, TwoConnectedComponents) { + const int a = 0; + const int b = 1; + const int c = 2; + const int d = 3; + const int e = 4; + const int num_nodes = 5; + const std::vector arcs_with_length = { + {a, b, 0.0}, {b, c, 0.0}, {d, e, 0.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, /*source=*/a, + /*destination=*/e, /*path_count=*/2), + ElementsAre(FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty()))); + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, /*source=*/b, + /*destination=*/d, /*path_count=*/2), + ElementsAre(FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty()))); +} + +TEST(KShortestPathOnDagTest, SetsNotConnectedDueToInfiniteCost) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length = {{a, destination, 1.0}, + {source, a, kInf}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/2), + ElementsAre(FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty()))); +} + +TEST(KShortestPathOnDagTest, AvoidInfiniteCost) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int b = 3; + const int num_nodes = 4; + const std::vector arcs_with_length = {{a, destination, 1.0}, + {b, destination, 1.0}, + {source, a, kInf}, + {source, b, 3.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/4.0, /*arc_path=*/ElementsAre(3, 1), + /*node_path=*/ElementsAre(source, b, destination)))); +} + +TEST(KShortestPathOnDagTest, SourceNotFirst) { + const int destination = 0; + const int source = 1; + const int num_nodes = 2; + const std::vector arcs_with_length = { + {source, destination, 1.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/1.0, /*arc_path=*/ElementsAre(0), + /*node_path=*/ElementsAre(source, destination)))); +} + +TEST(KShortestPathOnDagTest, MultipleArcs) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length = { + {source, destination, 4.0}, + {source, destination, 3.0}, + {source, destination, -2.0}, + {source, destination, 0.0}}; + + EXPECT_THAT(KShortestPathsOnDag(num_nodes, arcs_with_length, source, + destination, /*path_count=*/3), + ElementsAre(FieldsAre( + /*length=*/-2.0, /*arc_path=*/ElementsAre(2), + /*node_path=*/ElementsAre(source, destination)), + FieldsAre( + /*length=*/0.0, /*arc_path=*/ElementsAre(3), + /*node_path=*/ElementsAre(source, destination)), + FieldsAre( + /*length=*/3.0, /*arc_path=*/ElementsAre(1), + /*node_path=*/ElementsAre(source, destination)))); +} + +TEST(KShortestPathOnDagTest, UpdateCost) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int b = 3; + const int num_nodes = 4; + std::vector arcs_with_length = {{source, a, 5.0}, + {source, b, 2.0}, + {a, destination, 3.0}, + {b, destination, 20.0}}; + + EXPECT_THAT( + KShortestPathsOnDag(num_nodes, arcs_with_length, source, destination, + /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination)), + FieldsAre( + /*length=*/22.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination)))); + + // Update the length of arc b -> destination from 20.0 to -1.0. + arcs_with_length[3].length = -1.0; + + EXPECT_THAT( + KShortestPathsOnDag(num_nodes, arcs_with_length, source, destination, + /*path_count=*/2), + ElementsAre(FieldsAre( + /*length=*/1.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination)), + FieldsAre( + /*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination)))); +} + +TEST(KShortestPathsOnDagWrapperTest, MultipleSources) { + const int source_1 = 0; + const int source_2 = 1; + const int destination = 2; + const int num_nodes = 3; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/2); + std::vector arc_lengths; + graph.AddArc(source_1, destination); + arc_lengths.push_back(-6.0); + graph.AddArc(source_2, destination); + arc_lengths.push_back(3.0); + const std::vector topological_order = {source_2, source_1, destination}; + const int path_count = 2; + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &graph, &arc_lengths, topological_order, path_count); + shortest_paths_on_dag.RunKShortestPathOnDag({source_1, source_2}); + + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_paths_on_dag.LengthsTo(destination), + ElementsAre(-6.0, 3.0)); + EXPECT_THAT(shortest_paths_on_dag.ArcPathsTo(destination), + ElementsAre(ElementsAre(0), ElementsAre(1))); + EXPECT_THAT(shortest_paths_on_dag.NodePathsTo(destination), + ElementsAre(ElementsAre(source_1, destination), + ElementsAre(source_2, destination))); +} + +TEST(KShortestPathsOnDagWrapperTest, ShortestPathGoesThroughMultipleSources) { + const int source_1 = 0; + const int source_2 = 1; + const int destination = 2; + const int num_nodes = 3; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/3); + std::vector arc_lengths; + graph.AddArc(source_1, source_2); + arc_lengths.push_back(-7.0); + graph.AddArc(source_2, destination); + arc_lengths.push_back(3.0); + graph.AddArc(source_1, destination); + arc_lengths.push_back(5.0); + const std::vector topological_order = {source_1, source_2, destination}; + const int path_count = 2; + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &graph, &arc_lengths, topological_order, path_count); + shortest_paths_on_dag.RunKShortestPathOnDag({source_1, source_2}); + + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_paths_on_dag.LengthsTo(destination), + ElementsAre(-4.0, 3.0)); + EXPECT_THAT(shortest_paths_on_dag.ArcPathsTo(destination), + ElementsAre(ElementsAre(0, 1), ElementsAre(1))); + EXPECT_THAT(shortest_paths_on_dag.NodePathsTo(destination), + ElementsAre(ElementsAre(source_1, source_2, destination), + ElementsAre(source_2, destination))); +} + +TEST(KShortestPathsOnDagWrapperTest, MultipleDestinations) { + const int source = 0; + const int destination_1 = 1; + const int destination_2 = 2; + const int destination_3 = 3; + const int num_nodes = 4; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/3); + std::vector arc_lengths; + graph.AddArc(source, destination_1); + arc_lengths.push_back(3.0); + graph.AddArc(source, destination_2); + arc_lengths.push_back(1.0); + graph.AddArc(source, destination_3); + arc_lengths.push_back(2.0); + const std::vector topological_order = {source, destination_3, + destination_1, destination_2}; + const int path_count = 2; + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &graph, &arc_lengths, topological_order, path_count); + shortest_paths_on_dag.RunKShortestPathOnDag({source}); + + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(destination_1)); + EXPECT_THAT(shortest_paths_on_dag.LengthsTo(destination_1)[0], 3.0); + EXPECT_THAT(shortest_paths_on_dag.ArcPathsTo(destination_1)[0], + ElementsAre(0)); + EXPECT_THAT(shortest_paths_on_dag.NodePathsTo(destination_1)[0], + ElementsAre(source, destination_1)); + + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(destination_2)); + EXPECT_THAT(shortest_paths_on_dag.LengthsTo(destination_2)[0], 1.0); + EXPECT_THAT(shortest_paths_on_dag.ArcPathsTo(destination_2)[0], + ElementsAre(1)); + EXPECT_THAT(shortest_paths_on_dag.NodePathsTo(destination_2)[0], + ElementsAre(source, destination_2)); + + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(destination_3)); + EXPECT_THAT(shortest_paths_on_dag.LengthsTo(destination_3)[0], 2.0); + EXPECT_THAT(shortest_paths_on_dag.ArcPathsTo(destination_3)[0], + ElementsAre(2)); + EXPECT_THAT(shortest_paths_on_dag.NodePathsTo(destination_3)[0], + ElementsAre(source, destination_3)); +} + +TEST(KShortestPathsOnDagWrapperTest, RandomizedStressTest) { + absl::BitGen bit_gen; + const int kNumTests = 10000; + for (int test = 0; test < kNumTests; ++test) { + const int num_nodes = absl::Uniform(bit_gen, 2, 12); + const int num_arcs = absl::Uniform( + bit_gen, 1, std::min(num_nodes * (num_nodes - 1) / 2, 15)); + // Generate a random DAG with random lengths. + const auto [graph, topological_order] = BuildRandomDag(num_nodes, num_arcs); + const std::vector arc_lengths = GenerateRandomLengths(graph); + + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + const int path_count = 5; + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &graph, &arc_lengths, topological_order, path_count); + for (int _ = 0; _ < 20; ++_) { + // Draw sources (*with* repetition) with initial distances. + const int num_sources = absl::Uniform(bit_gen, 1, 5); + std::vector sources(num_sources); + for (int& source : sources) { + source = absl::Uniform(bit_gen, 0, num_nodes); + } + // Compute the number of paths + std::vector all_paths_count(num_nodes); + for (const int source : sources) { + all_paths_count[source] = 1; + } + for (const int from : topological_order) { + for (const int arc : graph.OutgoingArcs(from)) { + const int to = graph.Head(arc); + all_paths_count[to] += all_paths_count[from]; + } + } + + shortest_path_on_dag.RunShortestPathOnDag(sources); + shortest_paths_on_dag.RunKShortestPathOnDag(sources); + for (const int node : shortest_path_on_dag.reached_nodes()) { + EXPECT_TRUE(shortest_paths_on_dag.IsReachable(node)); + EXPECT_EQ(shortest_paths_on_dag.LengthsTo(node)[0], + shortest_path_on_dag.LengthTo(node)) + << node; + EXPECT_EQ(shortest_paths_on_dag.LengthsTo(node).size(), + std::min(all_paths_count[node], path_count)) + << node; + } + ASSERT_FALSE(HasFailure()) + << DUMP_VARS(num_nodes, num_arcs, num_sources, sources, arc_lengths) + << "\n With graph:\n" + << util::GraphToString(graph, util::PRINT_GRAPH_ARCS); + } + } +} + +// Debug tests. +#ifndef NDEBUG +TEST(KShortestPathOnDagTest, MinusInfWeight) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/{{0, 1, -kInf}}, + /*source=*/0, /*destination=*/1, /*path_count=*/2), + "-inf"); +} + +TEST(KShortestPathOnDagTest, NaNWeight) { + EXPECT_DEATH( + KShortestPathsOnDag(/*num_nodes=*/2, /*arcs_with_length=*/ + {{0, 1, std::numeric_limits::quiet_NaN()}}, + /*source=*/0, /*destination=*/1, /*path_count=*/2), + "NaN"); +} + +TEST(KShortestPathsOnDagWrapperTest, ValidateTopologicalOrder) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/1); + std::vector arc_lengths; + graph.AddArc(source, destination); + arc_lengths.push_back(1.0); + const std::vector topological_order = {source}; + const int path_count = 2; + + EXPECT_DEATH(KShortestPathsOnDagWrapper>( + &graph, &arc_lengths, topological_order, path_count), + "Invalid topological order"); +} +#endif // NDEBUG + +// ----------------------------------------------------------------------------- +// KShortestPathsOnDagWrapper benchmarks. +// ----------------------------------------------------------------------------- +void BM_RandomDag_K(benchmark::State& state) { + absl::BitGen bit_gen; + // Generate a fixed random DAG. + const int num_nodes = state.range(0); + const int num_arcs = num_nodes * state.range(1); + const int path_count = state.range(2); + const auto [graph, topological_order] = BuildRandomDag(num_nodes, num_arcs); + // Generate at most 20 scenarios of random arc lengths. + const int num_scenarios = std::min(20, (int)state.iterations()); + std::vector> arc_lengths_scenarios; + for (int _ = 0; _ < num_scenarios; ++_) { + arc_lengths_scenarios.push_back(GenerateRandomLengths(graph)); + } + std::vector arc_lengths = arc_lengths_scenarios.front(); + KShortestPathsOnDagWrapper> shortest_paths_on_dag( + &graph, &arc_lengths, topological_order, path_count); + for (auto _ : state) { + // Pick a arc lengths scenario at random. + arc_lengths = + arc_lengths_scenarios[absl::Uniform(bit_gen, 0, num_scenarios)]; + shortest_paths_on_dag.RunKShortestPathOnDag({0}); + CHECK(shortest_paths_on_dag.IsReachable(num_nodes - 1)); + const std::vector lengths = + shortest_paths_on_dag.LengthsTo(num_nodes - 1); + CHECK_GE(lengths[0], 0.0); + CHECK_LE(lengths[0], 10000.0); + } + state.SetItemsProcessed(state.iterations() * (num_nodes + num_arcs)); +} + +BENCHMARK(BM_RandomDag_K) + ->Args({1000, 10, 4}) + ->Args({1 << 16, 4, 4}) + ->Args({1 << 16, 4, 16}) + ->Args({1 << 16, 16, 4}) + ->Args({1 << 16, 16, 16}) + ->Args({1 << 22, 4, 4}) + ->Args({1 << 22, 4, 16}); + } // namespace } // namespace operations_research