From 36ca3496c052d22aeac01085cbe07b48f19ac0d4 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Thu, 13 Mar 2025 15:35:07 +0100 Subject: [PATCH] graph: add dag_* stuff --- ortools/graph/BUILD.bazel | 99 ++ ortools/graph/README.md | 10 + ortools/graph/dag_connectivity.cc | 122 ++ ortools/graph/dag_connectivity.h | 60 + ortools/graph/dag_connectivity_test.cc | 182 +++ .../graph/dag_constrained_shortest_path.cc | 95 ++ ortools/graph/dag_constrained_shortest_path.h | 932 +++++++++++++ .../dag_constrained_shortest_path_test.cc | 855 ++++++++++++ ortools/graph/dag_shortest_path.cc | 136 ++ ortools/graph/dag_shortest_path.h | 714 ++++++++++ ortools/graph/dag_shortest_path_test.cc | 1158 +++++++++++++++++ ortools/graph/samples/BUILD.bazel | 16 + ortools/graph/samples/code_samples.bzl | 4 + ...ag_constrained_shortest_path_sequential.cc | 139 ++ .../dag_multiple_shortest_paths_one_to_all.cc | 87 ++ .../dag_multiple_shortest_paths_sequential.cc | 135 ++ .../samples/dag_shortest_path_one_to_all.cc | 81 ++ .../samples/dag_shortest_path_sequential.cc | 120 ++ .../dag_simple_constrained_shortest_path.cc | 47 + .../dag_simple_multiple_shortest_paths.cc | 47 + .../graph/samples/dag_simple_shortest_path.cc | 44 + 21 files changed, 5083 insertions(+) create mode 100644 ortools/graph/dag_connectivity.cc create mode 100644 ortools/graph/dag_connectivity.h create mode 100644 ortools/graph/dag_connectivity_test.cc create mode 100644 ortools/graph/dag_constrained_shortest_path.cc create mode 100644 ortools/graph/dag_constrained_shortest_path.h create mode 100644 ortools/graph/dag_constrained_shortest_path_test.cc create mode 100644 ortools/graph/dag_shortest_path.cc create mode 100644 ortools/graph/dag_shortest_path.h create mode 100644 ortools/graph/dag_shortest_path_test.cc create mode 100644 ortools/graph/samples/dag_constrained_shortest_path_sequential.cc create mode 100644 ortools/graph/samples/dag_multiple_shortest_paths_one_to_all.cc create mode 100644 ortools/graph/samples/dag_multiple_shortest_paths_sequential.cc create mode 100644 ortools/graph/samples/dag_shortest_path_one_to_all.cc create mode 100644 ortools/graph/samples/dag_shortest_path_sequential.cc create mode 100644 ortools/graph/samples/dag_simple_constrained_shortest_path.cc create mode 100644 ortools/graph/samples/dag_simple_multiple_shortest_paths.cc create mode 100644 ortools/graph/samples/dag_simple_shortest_path.cc diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index dc59f80a26..10b66d8782 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -628,6 +628,31 @@ cc_test( ], ) +cc_library( + name = "dag_connectivity", + srcs = ["dag_connectivity.cc"], + hdrs = ["dag_connectivity.h"], + deps = [ + ":topologicalsorter", + "//ortools/base", + "//ortools/base:container_logging", + "//ortools/util:bitset", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "dag_connectivity_test", + srcs = ["dag_connectivity_test.cc"], + deps = [ + ":dag_connectivity", + "//ortools/base:gmock_main", + "//ortools/util:bitset", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "perfect_matching", srcs = ["perfect_matching.cc"], @@ -646,6 +671,40 @@ cc_library( ], ) +cc_library( + name = "dag_shortest_path", + srcs = ["dag_shortest_path.cc"], + hdrs = ["dag_shortest_path.h"], + deps = [ + ":graph", + ":topologicalsorter", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "dag_constrained_shortest_path", + srcs = ["dag_constrained_shortest_path.cc"], + hdrs = ["dag_constrained_shortest_path.h"], + deps = [ + ":dag_shortest_path", + ":graph", + ":topologicalsorter", + "//ortools/base:threadpool", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/base:log_severity", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "rooted_tree", hdrs = ["rooted_tree.h"], @@ -696,6 +755,46 @@ cc_test( ], ) +cc_test( + name = "dag_shortest_path_test", + size = "small", + srcs = ["dag_shortest_path_test.cc"], + deps = [ + ":dag_shortest_path", + ":graph", + ":io", + "//ortools/base:dump_vars", + "//ortools/base:gmock_main", + "//ortools/util:flat_matrix", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status", + "@com_google_absl//absl/types:span", + "@com_google_benchmark//:benchmark", + ], +) + +cc_test( + name = "dag_constrained_shortest_path_test", + srcs = ["dag_constrained_shortest_path_test.cc"], + deps = [ + ":dag_constrained_shortest_path", + ":graph", + ":io", + "//ortools/base:dump_vars", + "//ortools/base:gmock_main", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:cp_sat_solver", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/random", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + "@com_google_benchmark//:benchmark", + ], +) + # From util/graph cc_library( name = "connected_components", diff --git a/ortools/graph/README.md b/ortools/graph/README.md index a771613004..1f11415710 100644 --- a/ortools/graph/README.md +++ b/ortools/graph/README.md @@ -28,6 +28,16 @@ Generic algorithms for shortest paths: Specific algorithms for paths: +* [`dag_shortest_path.h`][dag_shortest_path_h]: shortest paths on directed + acyclic graphs. If you have such a graph, this implementation is likely to + be the fastest. Unlike most implementations, these algorithms have two + interfaces: a "simple" one (list of edges and weights) and a standard one + (taking as input a graph data structure from + [`//ortools/graph/graph.h`][graph_h]). + +* [`dag_constrained_shortest_path.`][dag_constrained_shortest_path_h]: + shortest paths on directed acyclic graphs with resource constraints. + * [`hamiltonian_path.h`][hamiltonian_path_h]: entry point for computing minimum [Hamiltonian paths](https://en.wikipedia.org/wiki/Hamiltonian_path) and cycles on directed graphs with costs on arcs, using a diff --git a/ortools/graph/dag_connectivity.cc b/ortools/graph/dag_connectivity.cc new file mode 100644 index 0000000000..ffa74b6d21 --- /dev/null +++ b/ortools/graph/dag_connectivity.cc @@ -0,0 +1,122 @@ +// 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. + +#include "ortools/graph/dag_connectivity.h" + +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "ortools/base/container_logging.h" +#include "ortools/base/logging.h" +#include "ortools/graph/topologicalsorter.h" + +namespace operations_research { + +// The algorithm is as follows: +// 1. Sort the nodes of the graph topologically. If a cycle is detected, +// terminate +// 2. Build the adjacency list for the graph, i.e., adj_list[i] is the list +// of nodes that can be directly reached from i. +// 3. Create a 2d bool vector x where x[i][j] indicates there is a path from +// i to j, and for each arc in "arcs", set x[i][j] to true +// 4. In reverse topological order (leaves first) for each node i, for each +// child j of i, for each node k reachable for j, set k to be reachable +// from i as well (x[i][k] = true for all k s.t. x[j][k] is true). +// +// The running times of the steps are: +// 1. O(num_arcs) +// 2. O(num_arcs) +// 3. O(num_nodes^2 + num_arcs) +// 4. O(num_nodes*num_arcs) +// Thus the total run time is O(num_nodes^2 + num_nodes*num_arcs). +// +// Implementation note: typically, step 4 will dominate. To speed up the inner +// loop, we use Bitset64, allowing use to merge 64 x[k][j] values at a time with +// the |= operator. +// +// For graphs where num_arcs is o(num_nodes), a different data structure could +// be used in 3, but this isn't really the interesting case (and prevents |=). +// +// A further improvement on this algorithm is possible, step four can run in +// time O(num_nodes*num_arcs_in_transitive_reduction), and as a by product, +// the transitive reduction can also be produced as output. For details, see +// "A REDUCT-AND_CLOSURE ALGORITHM FOR GRAPHS" (Alla Goralcikova and +// Vaclav Koubek 1979). The better typeset paper "AN IMPROVED ALGORITHM FOR +// TRANSITIVE CLOSURE ON ACYCLIC DIGRAPHS" (Klaus Simon 1988) gives a slight +// improvement on the result (less memory, same runtime). +std::vector> ComputeDagConnectivity( + absl::Span> arcs, bool* error_was_cyclic, + std::vector* error_cycle_out) { + CHECK(error_was_cyclic != nullptr); + CHECK(error_cycle_out != nullptr); + *error_was_cyclic = false; + error_cycle_out->clear(); + if (arcs.empty()) return {}; + int num_nodes = 0; + for (const std::pair& arc : arcs) { + CHECK_GE(arc.first, 0); + CHECK_GE(arc.second, 0); + num_nodes = std::max(num_nodes, arc.first + 1); + num_nodes = std::max(num_nodes, arc.second + 1); + } + DenseIntStableTopologicalSorter sorter(num_nodes); + for (const auto& arc : arcs) { + sorter.AddEdge(arc.first, arc.second); + } + std::vector topological_order; + int next; + while (sorter.GetNext(&next, error_was_cyclic, error_cycle_out)) { + topological_order.push_back(next); + } + if (*error_was_cyclic) return {}; + std::vector> adjacency_list(num_nodes); + for (const auto& arc : arcs) { + adjacency_list[arc.first].push_back(arc.second); + } + + std::vector> connectivity(num_nodes); + for (Bitset64& bitset : connectivity) { + bitset.Resize(num_nodes); + } + for (const auto& arc : arcs) { + connectivity[arc.first].Set(arc.second); + } + + // Iterate over the nodes in reverse topological order. + std::reverse(topological_order.begin(), topological_order.end()); + // NOTE(user): these two loops visit every arc in the graph, and each + // union is over a set of size given by the number of nodes. This gives the + // runtime in step 4 of O(num_nodes*num_arcs) + for (const int node : topological_order) { + for (const int child : adjacency_list[node]) { + connectivity[node].Union(connectivity[child]); + } + } + return connectivity; +} + +std::vector> ComputeDagConnectivityOrDie( + absl::Span> arcs) { + bool error_was_cyclic = false; + std::vector error_cycle; + std::vector> result = + ComputeDagConnectivity(arcs, &error_was_cyclic, &error_cycle); + CHECK(!error_was_cyclic) << "Graph should have been acyclic but has cycle: " + << gtl::LogContainer(error_cycle); + return result; +} + +} // namespace operations_research diff --git a/ortools/graph/dag_connectivity.h b/ortools/graph/dag_connectivity.h new file mode 100644 index 0000000000..02dfa2ddc7 --- /dev/null +++ b/ortools/graph/dag_connectivity.h @@ -0,0 +1,60 @@ +// 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 OR_TOOLS_GRAPH_DAG_CONNECTIVITY_H_ +#define OR_TOOLS_GRAPH_DAG_CONNECTIVITY_H_ + +#include +#include +#include + +#include "absl/types/span.h" +#include "ortools/util/bitset.h" + +namespace operations_research { + +// Given a directed graph, as defined by the arc list "arcs", computes either: +// 1. If the graph is acyclic, the matrix of values x, where x[i][j] indicates +// that there is a directed path from i to j. +// 2. If the graph is cyclic, "error_cycle_out" is set to contain the cycle, +// and the return value is empty. +// +// The algorithm runs in O(num_nodes^2 + num_nodes*num_arcs). +// +// Inputs: +// arcs: each a in "arcs" is a directed edge from a.first to a.second. Must +// have a.first, a.second >= 0. The graph is assumed to have nodes +// {0,1,...,max_{a in arcs} max(a.first, a.second)}, or have no nodes +// if arcs is the empty list. +// error_was_cyclic: output arg, is set to true if a cycle is detected. +// error_cycle_out: output arg, if a cycle is detected, error_cycle_out is +// set to contain the nodes of the cycle in order. +// +// Note: useful for computing the transitive closure of a binary relation, e.g. +// given the relation i < j for i,j in S that is transitive and some known +// values i < j, create a node for each i in S and an arc for each known +// relationship. Then any relationship implied by transitivity is given by +// the resulting matrix produced, or if the relation fails transitivity, a cycle +// proving this is produced. +std::vector> ComputeDagConnectivity( + absl::Span> arcs, bool* error_was_cyclic, + std::vector* error_cycle_out); + +// Like above, but will CHECK fail if the digraph with arc list "arcs" +// contains a cycle. +std::vector> ComputeDagConnectivityOrDie( + absl::Span> arcs); + +} // namespace operations_research + +#endif // OR_TOOLS_GRAPH_DAG_CONNECTIVITY_H_ diff --git a/ortools/graph/dag_connectivity_test.cc b/ortools/graph/dag_connectivity_test.cc new file mode 100644 index 0000000000..d8b43a7e74 --- /dev/null +++ b/ortools/graph/dag_connectivity_test.cc @@ -0,0 +1,182 @@ +// 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. + +#include "ortools/graph/dag_connectivity.h" + +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/util/bitset.h" + +using std::pair; +using std::vector; + +namespace operations_research { +namespace { + +TEST(DagConnectivityTest, EmptyGraph) { + vector> arc_list; + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + EXPECT_TRUE(conn.empty()); +} + +void CheckMatrix(vector> expected, + absl::Span> actual) { + int size = expected.size(); + ASSERT_EQ(size, actual.size()); + for (int i = 0; i < size; i++) { + SCOPED_TRACE(absl::StrCat("row", i)); + ASSERT_EQ(size, expected[i].size()); + ASSERT_EQ(size, actual[i].size()); + } + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + SCOPED_TRACE(absl::StrCat("entry:", i, ",", j)); + EXPECT_EQ(expected[i][j], actual[i][j]); + } + } +} + +TEST(DagConnectivityTest, SimpleGraph) { + vector> arc_list({{1, 0}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + CheckMatrix({{false, false}, {true, false}}, conn); +} + +TEST(DagConnectivityTest, SimpleSparseGraph) { + vector> arc_list({{3, 1}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + vector> expected(4, vector(4)); + expected[3][1] = true; + CheckMatrix(expected, conn); +} + +TEST(DagConnectivityTest, SelfCycle) { + vector> arc_list({{0, 1}, {1, 1}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_TRUE(error); + EXPECT_THAT(cycle, testing::ElementsAre(1)); +} + +TEST(DagConnectivityTest, LongCycle) { + vector> arc_list({{2, 3}, {3, 5}, {5, 2}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_TRUE(error); + EXPECT_THAT(cycle, testing::UnorderedElementsAre(2, 3, 5)); +} + +TEST(DagConnectivityTest, BasicTransitive) { + vector> arc_list({{0, 1}, {1, 2}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + vector> expected( + {{false, true, true}, {false, false, true}, {false, false, false}}); + CheckMatrix(expected, conn); +} + +TEST(DagConnectivityTest, SparseTransitive) { + vector> arc_list({{2, 5}, {5, 7}}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + vector> expected(8, vector(8)); + expected[2][5] = true; + expected[2][7] = true; + expected[5][7] = true; + CheckMatrix(expected, conn); +} + +TEST(DagConnectivityTest, RealGraph) { + vector> arc_list; + arc_list.push_back({8, 0}); + arc_list.push_back({1, 2}); + arc_list.push_back({1, 3}); + arc_list.push_back({3, 2}); + arc_list.push_back({4, 3}); + arc_list.push_back({4, 5}); + arc_list.push_back({5, 2}); + arc_list.push_back({7, 5}); + arc_list.push_back({5, 6}); + bool error = false; + vector cycle; + vector> conn = + ComputeDagConnectivity(arc_list, &error, &cycle); + ASSERT_FALSE(error); + EXPECT_TRUE(cycle.empty()); + vector> expected(9, vector(9)); + expected[1][2] = true; + expected[1][3] = true; + expected[3][2] = true; + expected[4][3] = true; + expected[4][2] = true; + expected[4][5] = true; + expected[4][6] = true; + expected[5][2] = true; + expected[5][6] = true; + expected[7][5] = true; + expected[7][2] = true; + expected[7][6] = true; + expected[8][0] = true; + CheckMatrix(expected, conn); +} + +TEST(ComputeDagConnectivityOrDie, SimpleGraph) { + vector> arc_list({{0, 1}, {1, 2}}); + vector> conn = ComputeDagConnectivityOrDie(arc_list); + vector> expected( + {{false, true, true}, {false, false, true}, {false, false, false}}); + CheckMatrix(expected, conn); +} + +TEST(ComputeDagConnectivityOrDieDeathTest, SimpleCycleCausesDeath) { + vector> arc_list({{2, 3}, {3, 5}, {5, 2}}); + EXPECT_DEATH( + { ComputeDagConnectivityOrDie(arc_list); }, + "Graph should have been acyclic but has cycle:"); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/graph/dag_constrained_shortest_path.cc b/ortools/graph/dag_constrained_shortest_path.cc new file mode 100644 index 0000000000..a64c935dbb --- /dev/null +++ b/ortools/graph/dag_constrained_shortest_path.cc @@ -0,0 +1,95 @@ +// 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. + +#include "ortools/graph/dag_constrained_shortest_path.h" + +#include +#include + +#include "absl/log/check.h" +#include "absl/status/statusor.h" +#include "absl/types/span.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/topologicalsorter.h" + +namespace operations_research { + +namespace { + +void ApplyMapping(absl::Span mapping, std::vector& values) { + if (!mapping.empty()) { + for (int i = 0; i < values.size(); ++i) { + values[i] = mapping[values[i]]; + } + } +} + +} // namespace + +PathWithLength ConstrainedShortestPathsOnDag( + const int num_nodes, + absl::Span arcs_with_length_and_resources, + int source, int destination, const std::vector& max_resources) { + using GraphType = util::StaticGraph<>; + using NodeIndex = GraphType::NodeIndex; + using ArcIndex = GraphType::ArcIndex; + + const int num_arcs = arcs_with_length_and_resources.size(); + GraphType graph(num_nodes, num_arcs); + std::vector arc_lengths; + arc_lengths.reserve(num_arcs); + std::vector> arc_resources(max_resources.size()); + for (int i = 0; i < max_resources.size(); ++i) { + arc_resources[i].reserve(num_arcs); + } + for (const auto& arc : arcs_with_length_and_resources) { + graph.AddArc(arc.from, arc.to); + arc_lengths.push_back(arc.length); + for (int i = 0; i < arc.resources.size(); ++i) { + arc_resources[i].push_back(arc.resources[i]); + } + } + + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &arc_lengths); + for (int i = 0; i < max_resources.size(); ++i) { + util::Permute(permutation, &arc_resources[i]); + } + + std::vector inverse_permutation = + internal::GetInversePermutation(permutation); + + const absl::StatusOr> topological_order = + util::graph::FastTopologicalSort(graph); + CHECK_OK(topological_order) << "arcs_with_length form a cycle."; + + std::vector sources = {source}; + std::vector destinations = {destination}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + *topological_order, sources, + destinations, &max_resources); + + GraphPathWithLength path_with_length = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + + ApplyMapping(inverse_permutation, path_with_length.arc_path); + + return {.length = path_with_length.length, + .arc_path = std::move(path_with_length.arc_path), + .node_path = std::move(path_with_length.node_path)}; +} + +} // namespace operations_research diff --git a/ortools/graph/dag_constrained_shortest_path.h b/ortools/graph/dag_constrained_shortest_path.h new file mode 100644 index 0000000000..c6bd452905 --- /dev/null +++ b/ortools/graph/dag_constrained_shortest_path.h @@ -0,0 +1,932 @@ +// 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 OR_TOOLS_GRAPH_DAG_CONSTRAINED_SHORTEST_PATH_H_ +#define OR_TOOLS_GRAPH_DAG_CONSTRAINED_SHORTEST_PATH_H_ + +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/base/log_severity.h" +#include "absl/log/check.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "ortools/base/threadpool.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" + +namespace operations_research { + +// This library provides APIs to compute the constrained shortest path (CSP) on +// a given directed acyclic graph (DAG) with resources on each arc. A CSP is a +// shortest path on a DAG which does not exceed a set of maximum resources +// consumption. The algorithm is exponential and has no guarantee to finish. It +// is based on bi-drectionnal search. First is a forward pass from the source to +// nodes “somewhere in the middle” to generate forward labels, just as the +// onedirectional labeling algorithm we discussed; then a symmetric backward +// pass from the destination generates backward labels; and finally at each node +// with both forward and backward labels, it joins any pair of labels to form a +// feasible complete path. Intuitively, the number of labels grows exponentially +// with the number of arcs in the path. The overall number of labels are then +// expected to be smaller with shorter paths. For DAG with a topological +// ordering, we can pick any node (usually right in the middle) as a *midpoint* +// to stop each pass at. Then labels can be joined at only one half of the nodes +// by considering all edges between each half. +// +// In the DAG, multiple arcs between the same pair of nodes is allowed. However, +// self-loop arcs are not allowed. +// +// Note that we use the length formalism here, but the arc lengths can represent +// any numeric physical quantity. A shortest path will just be a path minimizing +// this quantity where the length/resources of a path is the sum of the +// length/resources of its arcs. An arc length can be negative, or +inf +// (indicating that it should not be used). An arc length cannot be -inf or nan. +// +// Resources on each arc must be non-negative and cannot be +inf or nan. + +// ----------------------------------------------------------------------------- +// Basic API. +// ----------------------------------------------------------------------------- + +// `tail` and `head` should both be in [0, num_nodes) +// If the length is +inf, then the arc is not used. +struct ArcWithLengthAndResources { + int from = 0; + int to = 0; + double length = 0.0; + std::vector resources; +}; + +// Returns {+inf, {}, {}} if there is no path of finite length from the source +// to the destination. Dies if `arcs_with_length_and_resources` has a cycle. +PathWithLength ConstrainedShortestPathsOnDag( + int num_nodes, + absl::Span arcs_with_length_and_resources, + int source, int destination, const std::vector& max_resources); + +// ----------------------------------------------------------------------------- +// Advanced API. +// ----------------------------------------------------------------------------- +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +struct GraphPathWithLength { + double length = 0.0; + // The returned arc indices points into the `arcs_with_length` passed to the + // function below. + std::vector arc_path; + std::vector + node_path; // includes the source node. +}; + +// 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`. +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +class ConstrainedShortestPathsOnDagWrapper { + public: + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + + // IMPORTANT: All arguments must outlive the class. + // + // The vectors of `arc_lengths` and `arc_resources[i]` (for all resource i) + // *must* be of size `graph.num_arcs()` and indexed the same way as in + // `graph`. The vector `arc_resources` and `max_resources` *must* be of same + // size. + // + // 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 DCHECKed. + // + // If the number of labels in memory exceeds `max_num_created_labels / 2` at + // any point in each pass of the algorithm, new labels are not generated + // anymore and it returns the best path found so far, most particularly the + // empty path if none were found. + // + // IMPORTANT: You cannot modify anything except `arc_lengths` between calls to + // the `RunConstrainedShortestPathOnDag()` function. + ConstrainedShortestPathsOnDagWrapper( + const GraphType* graph, const std::vector* arc_lengths, + const std::vector>* arc_resources, + absl::Span topological_order, + absl::Span sources, + absl::Span destinations, + const std::vector* max_resources, + int max_num_created_labels = 1e9); + + // Returns {+inf, {}, {}} if there is no constrained path of finite length + // wihtin resources constraints from one node in `sources` to one node in + // `destinations`. + GraphPathWithLength RunConstrainedShortestPathOnDag(); + + // For benchmarking and informational purposes, returns the number of labels + // generated in the call of `RunConstrainedShortestPathOnDag()`. + int label_count() const { + return lengths_from_sources_[FORWARD].size() + + lengths_from_sources_[BACKWARD].size(); + } + + private: + enum Direction { + FORWARD = 0, + BACKWARD = 1, + }; + + inline static Direction Reverse(Direction d) { + return d == FORWARD ? BACKWARD : FORWARD; + } + + // A LabelPair includes the `length` of a path that can be constructed by + // merging the paths from two *linkable* labels corresponding to + // `label_index`. + struct LabelPair { + double length = 0.0; + int label_index[2]; + }; + + void RunHalfConstrainedShortestPathOnDag( + const GraphType& reverse_graph, absl::Span arc_lengths, + absl::Span> arc_resources, + absl::Span> min_arc_resources, + absl::Span max_resources, int max_num_created_labels, + std::vector& lengths_from_sources, + std::vector>& resources_from_sources, + std::vector& incoming_arc_indices_from_sources, + std::vector& incoming_label_indices_from_sources, + std::vector& first_label, std::vector& num_labels); + + // Returns the arc index linking two nodes from each pass forming the best + // path. Returns -1 if no better path than the one found from + // `best_label_pair` is found. + ArcIndex MergeHalfRuns( + const GraphType& graph, absl::Span arc_lengths, + absl::Span> arc_resources, + absl::Span max_resources, + const std::vector sub_node_indices[2], + const std::vector lengths_from_sources[2], + const std::vector> resources_from_sources[2], + const std::vector first_label[2], + const std::vector num_labels[2], LabelPair& best_label_pair); + + // Returns the path as list of arc indices that starts from a node in + // `sources` (if `direction` iS FORWARD) or `destinations` (if `direction` is + // BACKWARD) and ends in node represented by `best_label_index`. + std::vector ArcPathTo( + int best_label_index, + absl::Span incoming_arc_indices_from_sources, + absl::Span incoming_label_indices_from_sources) const; + + // Returns the list of all the nodes implied by a given `arc_path`. + std::vector NodePathImpliedBy(absl::Span arc_path, + const GraphType& graph) const; + + static constexpr double kTolerance = 1e-6; + + const GraphType* const graph_; + const std::vector* const arc_lengths_; + const std::vector>* const arc_resources_; + const std::vector* const max_resources_; + absl::Span sources_; + absl::Span destinations_; + const int num_resources_; + + // Data about *reachable* sub-graphs split in two for bidirectional search. + // Reachable nodes are nodes that can be reached given the resources + // constraints, i.e., for each resource, the sum of the minimum resource to + // get to a node from a node in `sources` and to get from a node to a node in + // `destinations` should be less than the maximum resource. Reachable arcs are + // arcs linking reachable nodes. + // + // `sub_reverse_graph_[dir]` is the reachable sub-graph split in *half* with + // an additional linked to sources (resp. destinations) for the forward (resp. + // backward) direction. For the forward (resp. backward) direction, nodes are + // indexed using the original (resp. reverse) topological order. + GraphType sub_reverse_graph_[2]; + std::vector> sub_arc_resources_[2]; + // `sub_full_arc_indices_[dir]` has size `sub_reverse_graph_[dir].num_arcs()` + // such that `sub_full_arc_indices_[dir][sub_arc] = arc` where `sub_arc` is + // the arc in the reachable sub-graph for direction `dir` (i.e. + // `sub_reverse_graph[dir]`) and `arc` is the arc in the original graph (i.e. + // `graph`). + std::vector sub_full_arc_indices_[2]; + // `sub_node_indices_[dir]` has size `graph->num_nodes()` such that + // `sub_node_indices[dir][node] = sub_node` where `node` is the node in the + // original graph (i.e. `graph`) and `sub_node` is the node in the reachable + // sub-graph for direction `dir` (i.e. `sub_reverse_graph[dir]`) and -1 if + // `node` is not present in reachable sub-graph. + std::vector sub_node_indices_[2]; + // `sub_is_source_[dir][sub_dir]` has size + // `sub_reverse_graph_[dir].num_nodes()` such that + // `sub_is_source_[dir][sub_dir][sub_node]` is true if `sub_node` is a node in + // the reachable sub-graph for direction `dir` (i.e. `sub_reverse_graph[dir]`) + // which is a source (resp. destination) is `sub_dir` is FORWARD (resp. + // BACKWARD). + std::vector sub_is_source_[2][2]; + // `sub_min_arc_resources_[dir]` has size `max_resources->size()` and + // `sub_min_arc_resources_[dir][r]`, `sub_reverse_graph_[dir].num_nodes()` + // such that `sub_min_arc_resources_[dir][r][sub_node]` is the minimum of + // resource r needed to get to a destination (resp. come from a source) if + // `dir` is FORWARD (resp. BACKWARD). + std::vector> sub_min_arc_resources_[2]; + // Maximum number of labels created for each sub-graph. + int max_num_created_labels_[2]; + + // Data about the last call of the RunConstrainedShortestPathOnDag() + // function. A path is only added to the following vectors if and only if + // it is feasible with respect to all resources. + // A Label includes the cumulative length, resources and the previous arc used + // in the path to get to this node. + // Instead of having a single vector of `Label` objects (cl/590819865), we + // split them into 3 vectors of more fundamental types as this improves + // push_back operations and memory release. + std::vector lengths_from_sources_[2]; + std::vector> resources_from_sources_[2]; + std::vector incoming_arc_indices_from_sources_[2]; + std::vector incoming_label_indices_from_sources_[2]; + std::vector node_first_label_[2]; + std::vector node_num_labels_[2]; +}; + +namespace internal { +template +std::vector GetInversePermutation(const std::vector& permutation); +} // namespace internal + +// ----------------------------------------------------------------------------- +// Implementation. +// ----------------------------------------------------------------------------- + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +ConstrainedShortestPathsOnDagWrapper:: + ConstrainedShortestPathsOnDagWrapper( + const GraphType* graph, const std::vector* arc_lengths, + const std::vector>* arc_resources, + absl::Span topological_order, + absl::Span sources, + absl::Span destinations, + const std::vector* max_resources, int max_num_created_labels) + : graph_(graph), + arc_lengths_(arc_lengths), + arc_resources_(arc_resources), + max_resources_(max_resources), + sources_(sources), + destinations_(destinations), + num_resources_(max_resources->size()) { + CHECK(graph_ != nullptr); + CHECK(arc_lengths_ != nullptr); + CHECK(arc_resources_ != nullptr); + CHECK(!sources_.empty()); + CHECK(!destinations_.empty()); + CHECK(max_resources_ != nullptr); + CHECK(!max_resources_->empty()) + << "max_resources cannot be empty. Use " + "ortools/graph/dag_shortest_path.h instead"; + if (DEBUG_MODE) { + CHECK_EQ(arc_lengths->size(), graph->num_arcs()); + CHECK_EQ(arc_resources->size(), max_resources->size()); + for (absl::Span arcs_resource : *arc_resources) { + CHECK_EQ(arcs_resource.size(), graph->num_arcs()); + for (const double arc_resource : arcs_resource) { + CHECK(arc_resource >= 0 && + arc_resource != std::numeric_limits::infinity() && + !std::isnan(arc_resource)) + << absl::StrFormat("resource cannot be negative nor +inf nor NaN"); + } + } + 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"; + for (const double max_resource : *max_resources) { + CHECK(max_resource >= 0 && + max_resource != std::numeric_limits::infinity() && + !std::isnan(max_resource)) + << absl::StrFormat( + "max_resource cannot be negative not +inf nor NaN"); + } + std::vector is_source(graph->num_nodes(), false); + for (const NodeIndex source : sources) { + is_source[source] = true; + } + for (const NodeIndex destination : destinations) { + CHECK(!is_source[destination]) + << "A node cannot be both a source and destination"; + } + } + + // Full graphs. + const GraphType* full_graph[2]; + const std::vector>* full_arc_resources[2]; + absl::Span full_topological_order[2]; + absl::Span full_sources[2]; + // Forward. + const int num_nodes = graph->num_nodes(); + const int num_arcs = graph->num_arcs(); + full_graph[FORWARD] = graph; + full_arc_resources[FORWARD] = arc_resources; + full_topological_order[FORWARD] = topological_order; + full_sources[FORWARD] = sources; + // Backward. + GraphType full_backward_graph(num_nodes, num_arcs); + for (ArcIndex arc_index = 0; arc_index < num_arcs; ++arc_index) { + full_backward_graph.AddArc(graph->Head(arc_index), graph->Tail(arc_index)); + } + std::vector full_permutation; + full_backward_graph.Build(&full_permutation); + const std::vector full_inverse_arc_indices = + internal::GetInversePermutation(full_permutation); + std::vector> backward_arc_resources(num_resources_); + for (int r = 0; r < num_resources_; ++r) { + backward_arc_resources[r] = (*arc_resources)[r]; + util::Permute(full_permutation, &backward_arc_resources[r]); + } + std::vector full_backward_topological_order; + full_backward_topological_order.reserve(num_nodes); + for (int i = num_nodes - 1; i >= 0; --i) { + full_backward_topological_order.push_back(topological_order[i]); + } + full_graph[BACKWARD] = &full_backward_graph; + full_arc_resources[BACKWARD] = &backward_arc_resources; + full_topological_order[BACKWARD] = full_backward_topological_order; + full_sources[BACKWARD] = destinations; + + // Get the minimum resources sources -> node and node -> destination for each + // node. + std::vector> full_min_arc_resources[2]; + for (const Direction dir : {FORWARD, BACKWARD}) { + full_min_arc_resources[dir].reserve(num_resources_); + std::vector full_arc_resource = full_arc_resources[dir]->front(); + ShortestPathsOnDagWrapper shortest_paths_on_dag( + full_graph[dir], &full_arc_resource, full_topological_order[dir]); + for (int r = 0; r < num_resources_; ++r) { + full_arc_resource = (*(full_arc_resources[dir]))[r]; + shortest_paths_on_dag.RunShortestPathOnDag(full_sources[dir]); + full_min_arc_resources[dir].push_back(shortest_paths_on_dag.LengthTo()); + } + } + + // Get reachable subgraph. + std::vector is_reachable(num_nodes, true); + std::vector sub_topological_order; + sub_topological_order.reserve(num_nodes); + for (const NodeIndex node_index : topological_order) { + for (int r = 0; r < num_resources_; ++r) { + if (full_min_arc_resources[FORWARD][r][node_index] + + full_min_arc_resources[BACKWARD][r][node_index] > + (*max_resources)[r]) { + is_reachable[node_index] = false; + break; + } + } + if (is_reachable[node_index]) { + sub_topological_order.push_back(node_index); + } + } + const int reachable_node_count = sub_topological_order.size(); + + // We split the number of labels evenly between each search (+1 for the + // additional source node). + max_num_created_labels_[BACKWARD] = max_num_created_labels / 2 + 1; + max_num_created_labels_[FORWARD] = + max_num_created_labels - max_num_created_labels / 2 + 1; + + // Split sub-graphs and related information. + // The split is based on the number of paths. This is used as a simple proxy + // for the number of labels. + int mid_index = 0; + { + // We use double to avoid overflow. Note that this is an heuristic, so we + // don't care too much if we are not precise enough. + std::vector path_count[2]; + for (const Direction dir : {FORWARD, BACKWARD}) { + const GraphType& reverse_full_graph = *(full_graph[Reverse(dir)]); + path_count[dir].resize(num_nodes); + for (const NodeIndex source : full_sources[dir]) { + ++path_count[dir][source]; + } + for (const NodeIndex to : full_topological_order[dir]) { + if (!is_reachable[to]) continue; + for (const ArcIndex arc : reverse_full_graph.OutgoingArcs(to)) { + const NodeIndex from = reverse_full_graph.Head(arc); + if (!is_reachable[from]) continue; + path_count[dir][to] += path_count[dir][from]; + } + } + } + for (const NodeIndex node_index : sub_topological_order) { + if (path_count[FORWARD][node_index] > path_count[BACKWARD][node_index]) { + break; + } + ++mid_index; + } + if (mid_index == reachable_node_count) { + mid_index = reachable_node_count / 2; + } + } + + for (const Direction dir : {FORWARD, BACKWARD}) { + absl::Span const sub_nodes = + dir == FORWARD + ? absl::MakeSpan(sub_topological_order).subspan(0, mid_index) + : absl::MakeSpan(sub_topological_order) + .subspan(mid_index, reachable_node_count - mid_index); + sub_node_indices_[dir].assign(num_nodes, -1); + sub_min_arc_resources_[dir].resize(num_resources_); + for (int r = 0; r < num_resources_; ++r) { + sub_min_arc_resources_[dir][r].resize(sub_nodes.size()); + } + for (NodeIndex i = 0; i < sub_nodes.size(); ++i) { + const NodeIndex sub_node_index = + dir == FORWARD ? i : sub_nodes.size() - 1 - i; + sub_node_indices_[dir][sub_nodes[i]] = sub_node_index; + for (int r = 0; r < num_resources_; ++r) { + sub_min_arc_resources_[dir][r][sub_node_index] = + full_min_arc_resources[Reverse(dir)][r][sub_nodes[i]]; + } + } + // IMPORTANT: The sub-graph has an additional node linked to sources (resp. + // destinations) for the forward (resp. backward) direction. This additional + // node is indexed with the last index. All added arcs are given to have an + // arc index in the original graph of -1. + const int sub_arcs_count = num_arcs + full_sources[dir].size(); + sub_reverse_graph_[dir] = GraphType(sub_nodes.size() + 1, sub_arcs_count); + sub_arc_resources_[dir].resize(num_resources_); + for (int r = 0; r < num_resources_; ++r) { + sub_arc_resources_[dir][r].reserve(sub_arcs_count); + } + sub_full_arc_indices_[dir].reserve(sub_arcs_count); + const GraphType& reverse_full_graph = *(full_graph[Reverse(dir)]); + for (ArcIndex arc_index = 0; arc_index < num_arcs; ++arc_index) { + const NodeIndex from = + sub_node_indices_[dir][reverse_full_graph.Tail(arc_index)]; + const NodeIndex to = + sub_node_indices_[dir][reverse_full_graph.Head(arc_index)]; + if (from == -1 || to == -1) { + continue; + } + sub_reverse_graph_[dir].AddArc(from, to); + ArcIndex sub_full_arc_index; + if (dir == FORWARD && !full_inverse_arc_indices.empty()) { + sub_full_arc_index = full_inverse_arc_indices[arc_index]; + } else { + sub_full_arc_index = arc_index; + } + for (int r = 0; r < num_resources_; ++r) { + sub_arc_resources_[dir][r].push_back( + (*arc_resources_)[r][sub_full_arc_index]); + } + sub_full_arc_indices_[dir].push_back(sub_full_arc_index); + } + for (const NodeIndex source : full_sources[dir]) { + const NodeIndex sub_source = sub_node_indices_[dir][source]; + if (sub_source == -1) { + continue; + } + sub_reverse_graph_[dir].AddArc(sub_source, sub_nodes.size()); + for (int r = 0; r < num_resources_; ++r) { + sub_arc_resources_[dir][r].push_back(0.0); + } + sub_full_arc_indices_[dir].push_back(-1); + } + std::vector sub_permutation; + sub_reverse_graph_[dir].Build(&sub_permutation); + for (int r = 0; r < num_resources_; ++r) { + util::Permute(sub_permutation, &sub_arc_resources_[dir][r]); + } + util::Permute(sub_permutation, &sub_full_arc_indices_[dir]); + } + + // Memory allocation is done here and only once in order to avoid + // reallocation at each call of `RunConstrainedShortestPathOnDag()` for + // better performance. + for (const Direction dir : {FORWARD, BACKWARD}) { + resources_from_sources_[dir].resize(num_resources_); + node_first_label_[dir].resize(sub_reverse_graph_[dir].size()); + node_num_labels_[dir].resize(sub_reverse_graph_[dir].size()); + } +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +GraphPathWithLength ConstrainedShortestPathsOnDagWrapper< + GraphType>::RunConstrainedShortestPathOnDag() { + // Assign lengths on sub-relevant graphs. + std::vector sub_arc_lengths[2]; + for (const Direction dir : {FORWARD, BACKWARD}) { + sub_arc_lengths[dir].reserve(sub_reverse_graph_[dir].num_arcs()); + for (ArcIndex sub_arc_index = 0; + sub_arc_index < sub_reverse_graph_[dir].num_arcs(); ++sub_arc_index) { + const ArcIndex arc_index = sub_full_arc_indices_[dir][sub_arc_index]; + if (arc_index == -1) { + sub_arc_lengths[dir].push_back(0.0); + continue; + } + sub_arc_lengths[dir].push_back((*arc_lengths_)[arc_index]); + } + } + + { + ThreadPool search_threads(2); + search_threads.StartWorkers(); + for (const Direction dir : {FORWARD, BACKWARD}) { + search_threads.Schedule([this, dir, &sub_arc_lengths]() { + RunHalfConstrainedShortestPathOnDag( + /*reverse_graph=*/sub_reverse_graph_[dir], + /*arc_lengths=*/sub_arc_lengths[dir], + /*arc_resources=*/sub_arc_resources_[dir], + /*min_arc_resources=*/sub_min_arc_resources_[dir], + /*max_resources=*/*max_resources_, + /*max_num_created_labels=*/max_num_created_labels_[dir], + /*lengths_from_sources=*/lengths_from_sources_[dir], + /*resources_from_sources=*/resources_from_sources_[dir], + /*incoming_arc_indices_from_sources=*/ + incoming_arc_indices_from_sources_[dir], + /*incoming_label_indices_from_sources=*/ + incoming_label_indices_from_sources_[dir], + /*first_label=*/node_first_label_[dir], + /*num_labels=*/node_num_labels_[dir]); + }); + } + } + + // Check destinations within relevant half sub-graphs. + LabelPair best_label_pair = { + .length = std::numeric_limits::infinity(), + .label_index = {-1, -1}}; + for (const Direction dir : {FORWARD, BACKWARD}) { + absl::Span destinations = + dir == FORWARD ? destinations_ : sources_; + for (const NodeIndex dst : destinations) { + const NodeIndex sub_dst = sub_node_indices_[dir][dst]; + if (sub_dst == -1) { + continue; + } + const int num_labels_dst = node_num_labels_[dir][sub_dst]; + if (num_labels_dst == 0) { + continue; + } + const int first_label_dst = node_first_label_[dir][sub_dst]; + for (int label_index = first_label_dst; + label_index < first_label_dst + num_labels_dst; ++label_index) { + const double length_dst = lengths_from_sources_[dir][label_index]; + if (length_dst < best_label_pair.length) { + best_label_pair.length = length_dst; + best_label_pair.label_index[dir] = label_index; + } + } + } + } + + const ArcIndex merging_arc_index = MergeHalfRuns( + /*graph=*/*graph_, /*arc_lengths=*/*arc_lengths_, + /*arc_resources=*/*arc_resources_, + /*max_resources=*/*max_resources_, + /*sub_node_indices=*/sub_node_indices_, + /*lengths_from_sources=*/lengths_from_sources_, + /*resources_from_sources=*/resources_from_sources_, + /*first_label=*/node_first_label_, + /*num_labels=*/node_num_labels_, /*best_label_pair=*/best_label_pair); + + std::vector arc_path; + for (const Direction dir : {FORWARD, BACKWARD}) { + for (const ArcIndex sub_arc_index : ArcPathTo( + /*best_label_index=*/best_label_pair.label_index[dir], + /*incoming_arc_indices_from_sources=*/ + incoming_arc_indices_from_sources_[dir], + /*incoming_label_indices_from_sources=*/ + incoming_label_indices_from_sources_[dir])) { + const ArcIndex arc_index = sub_full_arc_indices_[dir][sub_arc_index]; + if (arc_index == -1) { + break; + } + arc_path.push_back(arc_index); + } + if (dir == FORWARD && merging_arc_index != -1) { + absl::c_reverse(arc_path); + arc_path.push_back(merging_arc_index); + } + } + + // Clear all labels from the next run. + for (const Direction dir : {FORWARD, BACKWARD}) { + lengths_from_sources_[dir].clear(); + for (int r = 0; r < num_resources_; ++r) { + resources_from_sources_[dir][r].clear(); + } + incoming_arc_indices_from_sources_[dir].clear(); + incoming_label_indices_from_sources_[dir].clear(); + } + return {.length = best_label_pair.length, + .arc_path = arc_path, + .node_path = NodePathImpliedBy(arc_path, *graph_)}; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +void ConstrainedShortestPathsOnDagWrapper:: + RunHalfConstrainedShortestPathOnDag( + const GraphType& reverse_graph, absl::Span arc_lengths, + absl::Span> arc_resources, + absl::Span> min_arc_resources, + absl::Span max_resources, + const int max_num_created_labels, + std::vector& lengths_from_sources, + std::vector>& resources_from_sources, + std::vector& incoming_arc_indices_from_sources, + std::vector& incoming_label_indices_from_sources, + std::vector& first_label, std::vector& num_labels) { + // Initialize source node. + const NodeIndex source_node = reverse_graph.num_nodes() - 1; + first_label[source_node] = 0; + num_labels[source_node] = 1; + lengths_from_sources.push_back(0); + for (int r = 0; r < num_resources_; ++r) { + resources_from_sources[r].push_back(0); + } + incoming_arc_indices_from_sources.push_back(-1); + incoming_label_indices_from_sources.push_back(-1); + + std::vector lengths_to; + std::vector> resources_to(num_resources_); + std::vector incoming_arc_indices_to; + std::vector incoming_label_indices_to; + std::vector label_indices_to; + std::vector resources(num_resources_); + for (NodeIndex to = 0; to < source_node; ++to) { + lengths_to.clear(); + for (int r = 0; r < num_resources_; ++r) { + resources_to[r].clear(); + } + incoming_arc_indices_to.clear(); + incoming_label_indices_to.clear(); + for (const ArcIndex reverse_arc_index : reverse_graph.OutgoingArcs(to)) { + const NodeIndex from = reverse_graph.Head(reverse_arc_index); + const double arc_length = arc_lengths[reverse_arc_index]; + DCHECK(arc_length != -std::numeric_limits::infinity()); + if (arc_length == std::numeric_limits::infinity()) { + continue; + } + for (int label_index = first_label[from]; + label_index < first_label[from] + num_labels[from]; ++label_index) { + bool path_is_feasible = true; + for (int r = 0; r < num_resources_; ++r) { + DCHECK_GE(arc_resources[r][reverse_arc_index], 0.0); + resources[r] = resources_from_sources[r][label_index] + + arc_resources[r][reverse_arc_index]; + if (resources[r] + min_arc_resources[r][to] > max_resources[r]) { + path_is_feasible = false; + break; + } + } + if (!path_is_feasible) { + continue; + } + lengths_to.push_back(lengths_from_sources[label_index] + arc_length); + for (int r = 0; r < num_resources_; ++r) { + resources_to[r].push_back(resources[r]); + } + incoming_arc_indices_to.push_back(reverse_arc_index); + incoming_label_indices_to.push_back(label_index); + } + } + // Sort labels lexicographically with lengths then resources. + label_indices_to.clear(); + label_indices_to.reserve(lengths_to.size()); + for (int i = 0; i < lengths_to.size(); ++i) { + label_indices_to.push_back(i); + } + absl::c_sort(label_indices_to, [&](const int i, const int j) { + if (lengths_to[i] < lengths_to[j]) return true; + if (lengths_to[i] > lengths_to[j]) return false; + for (int r = 0; r < num_resources_; ++r) { + if (resources_to[r][i] < resources_to[r][j]) return true; + if (resources_to[r][i] > resources_to[r][j]) return false; + } + return i < j; + }); + + first_label[to] = lengths_from_sources.size(); + int& num_labels_to = num_labels[to]; + // Reset the number of labels to zero otherwise it holds the previous run + // result. + num_labels_to = 0; + for (int i = 0; i < label_indices_to.size(); ++i) { + // Check if label "i" on node `to` is dominated by any other label. + const int label_i_index = label_indices_to[i]; + bool label_i_is_dominated = false; + for (int j = 0; j < i - 1; ++j) { + const int label_j_index = label_indices_to[j]; + if (lengths_to[label_i_index] <= lengths_to[label_j_index]) continue; + bool label_j_dominates_label_i = true; + for (int r = 0; r < num_resources_; ++r) { + if (resources_to[r][label_i_index] <= + resources_to[r][label_j_index]) { + label_j_dominates_label_i = false; + break; + } + } + if (label_j_dominates_label_i) { + label_i_is_dominated = true; + break; + } + } + if (label_i_is_dominated) continue; + lengths_from_sources.push_back(lengths_to[label_i_index]); + for (int r = 0; r < num_resources_; ++r) { + resources_from_sources[r].push_back(resources_to[r][label_i_index]); + } + incoming_arc_indices_from_sources.push_back( + incoming_arc_indices_to[label_i_index]); + incoming_label_indices_from_sources.push_back( + incoming_label_indices_to[label_i_index]); + ++num_labels_to; + if (lengths_from_sources.size() >= max_num_created_labels) { + return; + } + } + } +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +typename GraphType::ArcIndex +ConstrainedShortestPathsOnDagWrapper::MergeHalfRuns( + const GraphType& graph, absl::Span arc_lengths, + absl::Span> arc_resources, + absl::Span max_resources, + const std::vector sub_node_indices[2], + const std::vector lengths_from_sources[2], + const std::vector> resources_from_sources[2], + const std::vector first_label[2], const std::vector num_labels[2], + LabelPair& best_label_pair) { + const std::vector& forward_sub_node_indices = + sub_node_indices[FORWARD]; + absl::Span forward_lengths = lengths_from_sources[FORWARD]; + const std::vector>& forward_resources = + resources_from_sources[FORWARD]; + absl::Span forward_first_label = first_label[FORWARD]; + absl::Span forward_num_labels = num_labels[FORWARD]; + const std::vector& backward_sub_node_indices = + sub_node_indices[BACKWARD]; + absl::Span backward_lengths = lengths_from_sources[BACKWARD]; + const std::vector>& backward_resources = + resources_from_sources[BACKWARD]; + absl::Span backward_first_label = first_label[BACKWARD]; + absl::Span backward_num_labels = num_labels[BACKWARD]; + ArcIndex merging_arc_index = -1; + for (ArcIndex arc_index = 0; arc_index < graph.num_arcs(); ++arc_index) { + const NodeIndex sub_from = forward_sub_node_indices[graph.Tail(arc_index)]; + if (sub_from == -1) { + continue; + } + const NodeIndex sub_to = backward_sub_node_indices[graph.Head(arc_index)]; + if (sub_to == -1) { + continue; + } + const int num_labels_from = forward_num_labels[sub_from]; + if (num_labels_from == 0) { + continue; + } + const int num_labels_to = backward_num_labels[sub_to]; + if (num_labels_to == 0) { + continue; + } + const double arc_length = arc_lengths[arc_index]; + DCHECK(arc_length != -std::numeric_limits::infinity()); + if (arc_length == std::numeric_limits::infinity()) { + continue; + } + const int first_label_from = forward_first_label[sub_from]; + const int first_label_to = backward_first_label[sub_to]; + for (int label_to_index = first_label_to; + label_to_index < first_label_to + num_labels_to; ++label_to_index) { + const double length_to = backward_lengths[label_to_index]; + if (arc_length + length_to >= best_label_pair.length) { + continue; + } + for (int label_from_index = first_label_from; + label_from_index < first_label_from + num_labels_from; + ++label_from_index) { + const double length_from = forward_lengths[label_from_index]; + if (length_from + arc_length + length_to >= best_label_pair.length) { + continue; + } + bool path_is_feasible = true; + for (int r = 0; r < num_resources_; ++r) { + DCHECK_GE(arc_resources[r][arc_index], 0.0); + if (forward_resources[r][label_from_index] + + arc_resources[r][arc_index] + + backward_resources[r][label_to_index] > + max_resources[r]) { + path_is_feasible = false; + break; + } + } + if (!path_is_feasible) { + continue; + } + best_label_pair.length = length_from + arc_length + length_to; + best_label_pair.label_index[FORWARD] = label_from_index; + best_label_pair.label_index[BACKWARD] = label_to_index; + merging_arc_index = arc_index; + } + } + } + return merging_arc_index; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector +ConstrainedShortestPathsOnDagWrapper::ArcPathTo( + const int best_label_index, + absl::Span incoming_arc_indices_from_sources, + absl::Span incoming_label_indices_from_sources) const { + int current_label_index = best_label_index; + std::vector arc_path; + for (int i = 0; i < graph_->num_nodes(); ++i) { + if (current_label_index == -1) { + break; + } + arc_path.push_back(incoming_arc_indices_from_sources[current_label_index]); + current_label_index = + incoming_label_indices_from_sources[current_label_index]; + } + return arc_path; +} + +template +#if __cplusplus >= 202002L + requires DagGraphType +#endif +std::vector +ConstrainedShortestPathsOnDagWrapper::NodePathImpliedBy( + absl::Span arc_path, const GraphType& graph) const { + if (arc_path.empty()) { + return {}; + } + std::vector node_path; + node_path.reserve(arc_path.size() + 1); + for (const ArcIndex arc_index : arc_path) { + node_path.push_back(graph.Tail(arc_index)); + } + node_path.push_back(graph.Head(arc_path.back())); + return node_path; +} + +namespace internal { + +template +std::vector GetInversePermutation(const std::vector& permutation) { + std::vector inverse_permutation(permutation.size()); + if (!permutation.empty()) { + for (T i = 0; i < permutation.size(); ++i) { + inverse_permutation[permutation[i]] = i; + } + } + return inverse_permutation; +} + +} // namespace internal + +} // namespace operations_research + +#endif // OR_TOOLS_GRAPH_DAG_CONSTRAINED_SHORTEST_PATH_H_ diff --git a/ortools/graph/dag_constrained_shortest_path_test.cc b/ortools/graph/dag_constrained_shortest_path_test.cc new file mode 100644 index 0000000000..e1e2ab7579 --- /dev/null +++ b/ortools/graph/dag_constrained_shortest_path_test.cc @@ -0,0 +1,855 @@ +// 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. + +#include "ortools/graph/dag_constrained_shortest_path.h" + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/log/check.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/dump_vars.h" +#include "ortools/base/gmock.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/graph_io.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research { +namespace { + +constexpr double kInf = std::numeric_limits::infinity(); + +using ::testing::ElementsAre; +using ::testing::FieldsAre; +using ::testing::IsEmpty; + +TEST(GetInversePermutationTest, EmptyPermutation) { + EXPECT_THAT(internal::GetInversePermutation({}), IsEmpty()); +} + +TEST(GetInversePermutationTest, SingleElementPermutation) { + EXPECT_THAT(internal::GetInversePermutation({0}), ElementsAre(0)); +} + +TEST(GetInversePermutationTest, ThreeElementPermutation) { + EXPECT_THAT(internal::GetInversePermutation({1, 2, 0}), + ElementsAre(2, 0, 1)); +} + +TEST(GetInversePermutationTest, RandomPermutation) { + const int num_tests = 100; + const int max_size = 1000; + absl::BitGen bitgen; + for (int unused = 0; unused < num_tests; ++unused) { + const int num_elements = absl::Uniform(bitgen, 0, max_size); + std::vector permutation(num_elements); + absl::c_iota(permutation, 0); + absl::c_shuffle(permutation, bitgen); + const std::vector inverse_permutation = + internal::GetInversePermutation(permutation); + for (int i = 0; i < num_elements; ++i) { + EXPECT_EQ(inverse_permutation[permutation[i]], i); + } + } +} + +TEST(ConstrainedShortestPathOnDagTest, SimpleGraph) { + 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_and_resources = + {{source, a, 5.0, {6.0}}, + {source, b, 2.0, {4.0}}, + {a, destination, 3.0, {2.0}}, + {b, destination, 20.0, {3.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{7.0}), + FieldsAre(/*length=*/22.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, SimpleGraphTwoPaths) { + 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_and_resources = + {{source, a, 5.0, {2.0}}, + {source, b, 2.0, {1.0}}, + {a, destination, 3.0, {1.0}}, + {b, destination, 20.0, {1.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{6.0}), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, 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_and_resources = + {{a, c, 5.0, {5.0}}, {source, b, 7.0, {4.0}}, + {a, b, 1.0, {3.0}}, {source, a, 3.0, {4.0}}, + {c, destination, 5.0, {1.0}}, {b, destination, -2.0, {1.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{6.0}), + FieldsAre(/*length=*/5.0, /*arc_path=*/ElementsAre(1, 5), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, LargerGraphWithDominance) { + 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_and_resources = + {{a, c, 5.0, {1.0}}, {source, b, 1.0, {3.0}}, + {a, b, 7.0, {4.0}}, {source, a, 3.0, {4.0}}, + {c, destination, 5.0, {1.0}}, {b, destination, -2.0, {1.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{6.0}), + FieldsAre(/*length=*/-1.0, /*arc_path=*/ElementsAre(1, 5), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, LargerGraphNoMaximumDuration) { + 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_and_resources = + {{a, c, 5.0, {1.0}}, {source, b, 7.0, {4.0}}, + {a, b, 1.0, {3.0}}, {source, a, 3.0, {4.0}}, + {c, destination, 5.0, {1.0}}, {b, destination, -2.0, {1.0}}}; + + EXPECT_THAT( + ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, destination, + /*max_resources=*/{std::numeric_limits::max()}), + FieldsAre(/*length=*/2.0, /*arc_path=*/ElementsAre(3, 2, 5), + /*node_path=*/ElementsAre(source, a, b, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, GraphWithInefficientEdge) { + const int source = 0; + const int a = 1; + const int destination = 2; + const int num_nodes = 3; + const std::vector arcs_with_length_and_resources = + {{source, a, 3.0, {4.0}}, + {source, destination, 9.0, {6.0}}, + {a, destination, 5.0, {1.0}}}; + + EXPECT_THAT( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, + /*max_resources=*/{6.0}), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, NoResources) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length_and_resources = + {{source, a, 5.0, {}}, {a, destination, 3.0, {}}}; + + EXPECT_DEATH( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, + /*max_resources=*/{}), + "ortools/graph/dag_shortest_path.h"); +} + +TEST(ConstrainedShortestPathOnDagTest, Cycle) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, 1.0, {0.0}}, {destination, source, 2.0, {1.0}}}; + + EXPECT_DEATH( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, + /*max_resources=*/{0.0}), + "cycle"); +} + +TEST(ConstrainedShortestPathOnDagTest, SetsNotConnected) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length_and_resources = + {{source, a, 1.0, {0.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{0.0}), + FieldsAre(/*length=*/kInf, + /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); +} + +TEST(ConstrainedShortestPathOnDagTest, SetsNotConnectedDueToInfiniteCost) { + const int source = 2; + const int destination = 0; + const int a = 1; + const int num_nodes = 3; + const std::vector arcs_with_length_and_resources = + {{a, destination, 1.0, {0.0}}, {source, a, kInf, {0.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{0.0}), + FieldsAre(/*length=*/kInf, + /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); +} + +TEST(ConstrainedShortestPathOnDagTest, SetsNotConnectedDueToLackOfResources) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, 1.0, {1.0, 8.0}}, + {source, destination, 2.0, {7.0, 2.0}}, + {source, destination, 3.0, {6.0, 3.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{4.0, 4.0}), + FieldsAre(/*length=*/kInf, + /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); +} + +TEST(ConstrainedShortestPathOnDagTest, MultipleArcs) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, 4.0, {2.0}}, {source, destination, 2.0, {4.0}}}; + + EXPECT_THAT(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{3.0}), + FieldsAre(/*length=*/4.0, /*arc_path=*/ElementsAre(0), + /*node_path=*/ElementsAre(source, destination))); +} + +TEST(ConstrainedShortestPathOnDagTest, UpdateArcsLengthAndResources) { + 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_and_resources = { + {source, a, 5.0, {1.0, 3.0}}, + {source, b, 2.0, {4.0, 10.0}}, + {a, destination, 3.0, {5.0, 9.0}}, + {b, destination, 20.0, {2.0, 2.0}}}; + const std::vector max_resources = {6.0, 12.0}; + + EXPECT_THAT( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, max_resources), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); + + // Update the length of arc b -> destination from 20.0 to -1.0. + arcs_with_length_and_resources[3].length = -1.0; + + EXPECT_THAT( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, max_resources), + FieldsAre(/*length=*/1.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination))); + + // Update the first resource of arc source -> b from 4.0 to 5.0 making + // the path source -> b -> destination infeasible. + arcs_with_length_and_resources[1].resources[0] = 5.0; + + EXPECT_THAT( + ConstrainedShortestPathsOnDag(num_nodes, arcs_with_length_and_resources, + source, destination, max_resources), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, + 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=*/2); + std::vector arc_lengths; + std::vector> arc_resources(1); + graph.AddArc(source_1, source_2); + arc_lengths.push_back(-7.0); + arc_resources[0].push_back(2.0); + graph.AddArc(source_2, destination); + arc_lengths.push_back(3.0); + arc_resources[0].push_back(3.0); + const std::vector topological_order = {source_1, source_2, destination}; + const std::vector sources = {source_1, source_2}; + const std::vector destinations = {destination}; + const std::vector max_resources = {6.0}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, destinations, + &max_resources); + + EXPECT_THAT( + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/-4.0, /*arc_path=*/ElementsAre(0, 1), + /*node_path=*/ElementsAre(source_1, source_2, destination))); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, 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; + std::vector> arc_resources(1); + graph.AddArc(source, destination_1); + arc_lengths.push_back(3.0); + arc_resources[0].push_back(5.0); + graph.AddArc(source, destination_2); + arc_lengths.push_back(1.0); + arc_resources[0].push_back(7.0); + graph.AddArc(source, destination_3); + arc_lengths.push_back(2.0); + arc_resources[0].push_back(6.0); + const std::vector sources = {source}; + const std::vector destinations = {destination_1, destination_2, + destination_3}; + const std::vector topological_order = {source, destination_3, + destination_1, destination_2}; + const std::vector max_resources = {6.0}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, destinations, + &max_resources); + + EXPECT_THAT( + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/2.0, /*arc_path=*/ElementsAre(2), + /*node_path=*/ElementsAre(source, destination_3))); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, UpdateArcsLength) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int b = 3; + const int num_nodes = 4; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/4); + std::vector arc_lengths; + std::vector> arc_resources(2); + graph.AddArc(source, a); + arc_lengths.push_back(5.0); + arc_resources[0].push_back(1.0); + arc_resources[1].push_back(3.0); + graph.AddArc(source, b); + arc_lengths.push_back(2.0); + arc_resources[0].push_back(4.0); + arc_resources[1].push_back(10.0); + graph.AddArc(a, destination); + arc_lengths.push_back(3.0); + arc_resources[0].push_back(5.0); + arc_resources[1].push_back(9.0); + graph.AddArc(b, destination); + arc_lengths.push_back(20.0); + arc_resources[0].push_back(2.0); + arc_resources[1].push_back(2.0); + const std::vector topological_order = {source, a, b, destination}; + const std::vector sources = {source}; + const std::vector destinations = {destination}; + const std::vector max_resources = {6.0, 12.0}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, destinations, + &max_resources); + + EXPECT_THAT( + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); + + // Update the length of arc b -> destination from 20.0 to -1.0. + arc_lengths[3] = -1.0; + + EXPECT_THAT( + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/1.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, LimitMaximumNumberOfLabels) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int num_nodes = 3; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/2); + std::vector arc_lengths; + std::vector> arc_resources(1); + graph.AddArc(source, a); + arc_lengths.push_back(5.0); + arc_resources[0].push_back(2.0); + graph.AddArc(a, destination); + arc_lengths.push_back(-2.0); + arc_resources[0].push_back(1.0); + const std::vector topological_order = {source, a, destination}; + const std::vector sources = {source}; + const std::vector destinations = {destination}; + const std::vector max_resources = {6.0}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag_with_one_label( + &graph, &arc_lengths, &arc_resources, topological_order, sources, + destinations, &max_resources, /*max_num_created_labels=*/1); + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag_with_four_labels( + &graph, &arc_lengths, &arc_resources, topological_order, sources, + destinations, &max_resources, /*max_num_created_labels=*/4); + + EXPECT_THAT(constrained_shortest_path_on_dag_with_one_label + .RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/kInf, + /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); + EXPECT_THAT(constrained_shortest_path_on_dag_with_four_labels + .RunConstrainedShortestPathOnDag(), + FieldsAre(/*length=*/3.0, /*arc_path=*/ElementsAre(0, 1), + /*node_path=*/ElementsAre(source, a, destination))); +} + +// 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. +template +std::pair, std::vector> +BuildRandomDag(const NodeIndex num_nodes, const ArcIndex num_arcs) { + absl::BitGen bit_gen; + CHECK_GE(num_nodes, 2); + CHECK_GE(num_arcs, 1); + CHECK_LE(num_arcs, (num_nodes * (num_nodes - 1)) / 2); + std::vector topological_order(num_nodes); + topological_order.back() = num_nodes - 1; + absl::Span non_start_end = + absl::MakeSpan(topological_order).subspan(1, num_nodes - 2); + absl::c_iota(non_start_end, 1); + absl::c_shuffle(non_start_end, bit_gen); + ArcIndex edges_added = 0; + util::StaticGraph graph(num_nodes, num_arcs); + graph.AddArc(0, num_nodes - 1); + while (edges_added < num_arcs - 1) { + NodeIndex start_index = absl::Uniform(bit_gen, 0, num_nodes - 1); + NodeIndex end_index = absl::Uniform(bit_gen, start_index + 1, num_nodes); + graph.AddArc(topological_order[start_index], topological_order[end_index]); + edges_added++; + } + graph.Build(); + return {graph, topological_order}; +} + +// The length of each arc is drawn uniformly at random within a given interval +// except if the first arc from 0 to num_nodes-1 where it is set to +// `start_to_end_value`. +template +std::vector GenerateRandomIntegerValues( + const GraphType& graph, const double min_value = 0.0, + const double max_value = 10.0, const double start_to_end_value = 10000.0) { + absl::BitGen bit_gen; + std::vector arc_values; + arc_values.reserve(graph.num_arcs()); + bool start_to_end_value_set = false; + for (typename GraphType::ArcIndex arc = 0; arc < graph.num_arcs(); ++arc) { + if (!start_to_end_value_set && graph.Tail(arc) == 0 && + graph.Head(arc) == graph.num_nodes() - 1) { + arc_values.push_back(start_to_end_value); + start_to_end_value_set = true; + continue; + } + arc_values.push_back( + static_cast(absl::Uniform(bit_gen, min_value, max_value))); + } + return arc_values; +} + +double SolveConstrainedShortestPathUsingIntegerProgramming( + const util::StaticGraph<>& graph, absl::Span arc_lengths, + absl::Span> arc_resources, + absl::Span max_resources, + absl::Span::NodeIndex> sources, + absl::Span::NodeIndex> destinations) { + using NodeIndex = util::StaticGraph<>::NodeIndex; + using ArcIndex = util::StaticGraph<>::ArcIndex; + + math_opt::Model model; + std::vector arc_variables; + std::vector flow_conservation(graph.num_nodes(), + 0.0); + for (ArcIndex arc_index = 0; arc_index < graph.num_arcs(); ++arc_index) { + arc_variables.push_back(model.AddBinaryVariable(absl::StrCat( + arc_index, "_", graph.Tail(arc_index), "->", graph.Head(arc_index)))); + model.set_objective_coefficient(arc_variables[arc_index], + arc_lengths[arc_index]); + flow_conservation[graph.Head(arc_index)] -= arc_variables[arc_index]; + flow_conservation[graph.Tail(arc_index)] += arc_variables[arc_index]; + } + + math_opt::LinearExpression all_sources; + math_opt::LinearExpression all_destinations; + for (NodeIndex node_index = 0; node_index < graph.num_nodes(); ++node_index) { + math_opt::LinearExpression net_flow = 0; + if (absl::c_linear_search(sources, node_index)) { + const math_opt::Variable s = model.AddBinaryVariable(); + all_sources += s; + net_flow += s; + } + if (absl::c_linear_search(destinations, node_index)) { + const math_opt::Variable t = model.AddBinaryVariable(); + all_destinations += t; + net_flow -= t; + } + model.AddLinearConstraint(flow_conservation[node_index] == net_flow); + } + model.AddLinearConstraint(all_sources == 1); + model.AddLinearConstraint(all_destinations == 1); + for (int r = 0; r < max_resources.size(); ++r) { + math_opt::LinearExpression variable_resources; + for (ArcIndex arc_index = 0; arc_index < graph.num_arcs(); ++arc_index) { + variable_resources += + arc_resources[r][arc_index] * arc_variables[arc_index]; + } + model.AddLinearConstraint(variable_resources <= max_resources[r]); + } + const absl::StatusOr result = + math_opt::Solve(model, math_opt::SolverType::kCpSat, {}); + CHECK_OK(result.status()) + << util::GraphToString(graph, util::PRINT_GRAPH_ARCS); + CHECK_OK(result->termination.EnsureIsOptimal()) + << util::GraphToString(graph, util::PRINT_GRAPH_ARCS); + return result->objective_value(); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, + RandomizedStressTestSingleResource) { + absl::BitGen bit_gen; + const int kNumTests = 50; + 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 resources + const auto [graph, topological_order] = BuildRandomDag(num_nodes, num_arcs); + std::vector arc_lengths(num_arcs); + std::vector> arc_resources(1); + arc_resources[0] = GenerateRandomIntegerValues(graph, /*min_value=*/1.0, + /*max_value=*/10.0, + /*start_to_end_value=*/1.0); + const std::vector sources = {0}; + const std::vector destinations = {num_nodes - 1}; + const std::vector max_resources = {15.0}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, + destinations, &max_resources); + const int kNumSamples = 5; + for (int _ = 0; _ < kNumSamples; ++_) { + arc_lengths = GenerateRandomIntegerValues(graph); + const GraphPathWithLength> path_with_length = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + + EXPECT_NEAR(path_with_length.length, + SolveConstrainedShortestPathUsingIntegerProgramming( + graph, arc_lengths, arc_resources, max_resources, sources, + destinations), + 1e-5); + + ASSERT_FALSE(HasFailure()) + << DUMP_VARS(num_nodes, num_arcs, arc_lengths) << "\n With graph :\n " + << util::GraphToString(graph, util::PRINT_GRAPH_ARCS); + } + } +} + +// ----------------------------------------------------------------------------- +// Benchmark. +// ----------------------------------------------------------------------------- +template +void BM_RandomDag(benchmark::State& state) { + absl::BitGen bit_gen; + // Generate a fixed random DAG. + const NodeIndex num_nodes = state.range(0); + const ArcIndex num_arcs = num_nodes * state.range(1); + const auto [graph, topological_order] = BuildRandomDag(num_nodes, num_arcs); + // Generate 20 scenarios of random arc lengths. + const int num_scenarios = 20; + std::vector> arc_lengths_scenarios; + for (int _ = 0; _ < num_scenarios; ++_) { + arc_lengths_scenarios.push_back(GenerateRandomIntegerValues(graph)); + } + std::vector arc_lengths(num_arcs); + std::vector> arc_resources(1); + arc_resources[0] = + GenerateRandomIntegerValues(graph, /*min_value=*/1.0, + /*max_value=*/10.0, + /*start_to_end_value=*/num_nodes * 0.2); + const std::vector sources = {0}; + const std::vector destinations = {num_nodes - 1}; + const std::vector max_resources = {num_nodes * 0.2}; + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, destinations, + &max_resources); + + int total_label_count = 0; + for (auto _ : state) { + // Pick a arc lengths scenario at random. + arc_lengths = + arc_lengths_scenarios[absl::Uniform(bit_gen, 0, num_scenarios)]; + const GraphPathWithLength> + path_with_length = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + total_label_count += constrained_shortest_path_on_dag.label_count(); + CHECK_GE(path_with_length.length, 0.0); + CHECK_LE(path_with_length.length, 10000.0); + } + state.SetItemsProcessed(std::max(1, total_label_count)); +} + +BENCHMARK(BM_RandomDag) + ->ArgPair(1 << 10, 16) + ->ArgPair(1 << 16, 4) + ->ArgPair(1 << 16, 16) + ->ArgPair(1 << 19, 4); +BENCHMARK(BM_RandomDag) + ->ArgPair(1 << 19, 16) + ->ArgPair(1 << 22, 4); + +// Generate a 2-dimensional grid DAG. +// Eg. for width=3, height=2, it generates this: +// 0 ----> 1 ----> 2 +// | | | +// | | | +// v v v +// 3 ----> 4 ----> 5 +void BM_GridDAG(benchmark::State& state) { + const int64_t width = state.range(0); + const int64_t height = state.range(1); + const int num_resources = state.range(2); + const int num_nodes = width * height; + const int num_arcs = 2 * num_nodes - width - height; + util::StaticGraph<> graph(num_nodes, num_arcs); + // Add horizontal edges. + for (int i = 0; i < height; ++i) { + for (int j = 1; j < width; ++j) { + const int left = i * width + (j - 1); + const int right = i * width + j; + graph.AddArc(left, right); + } + } + // Add vertical edges. + for (int i = 1; i < height; ++i) { + for (int j = 0; j < width; ++j) { + const int up = (i - 1) * width + j; + const int down = i * width + j; + graph.AddArc(up, down); + } + } + graph.Build(); + std::vector topological_order(num_nodes); + absl::c_iota(topological_order, 0); + + // Generate 20 scenarios of random arc lengths. + absl::BitGen bit_gen; + const int kNumScenarios = 20; + std::vector> arc_lengths_scenarios; + for (int unused = 0; unused < kNumScenarios; ++unused) { + std::vector arc_lengths(graph.num_arcs()); + for (int i = 0; i < graph.num_arcs(); ++i) { + arc_lengths[i] = absl::Uniform(bit_gen, 0, 1); + } + arc_lengths_scenarios.push_back(arc_lengths); + } + + std::vector> arc_resources(num_resources); + for (int r = 0; r < num_resources; ++r) { + arc_resources[r].resize(graph.num_arcs()); + for (int i = 0; i < graph.num_arcs(); ++i) { + arc_resources[r][i] = absl::Uniform(bit_gen, 0, 1); + } + } + + std::vector arc_lengths(num_arcs); + const std::vector sources = {0}; + const std::vector destinations = {num_nodes - 1}; + std::vector max_resources(num_resources); + // Each path from source to destination has `(width + height - 2)` arcs. Each + // arc has mean resource(s) 0.5. We want to consider paths with half (0.5) the + // mean resource(s). + const double max_resource = (width + height - 2) * 0.5 * 0.5; + for (int r = 0; r < num_resources; ++r) { + max_resources[r] = max_resource; + } + + ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &arc_lengths, &arc_resources, + topological_order, sources, destinations, + &max_resources); + + int total_label_count = 0; + for (auto _ : state) { + // Pick a arc lengths scenario at random. + arc_lengths = + arc_lengths_scenarios[absl::Uniform(bit_gen, 0, kNumScenarios)]; + const GraphPathWithLength> path_with_length = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + total_label_count += constrained_shortest_path_on_dag.label_count(); + CHECK_GE(path_with_length.length, 0.0); + } + state.SetItemsProcessed(std::max(1, total_label_count)); +} + +BENCHMARK(BM_GridDAG) + ->Args({100, 100, 1}) + ->Args({100, 100, 2}) + ->Args({1000, 100, 1}) + ->Args({1000, 100, 2}); + +// ----------------------------------------------------------------------------- +// Debug tests. +// ----------------------------------------------------------------------------- +#ifndef NDEBUG +TEST(ConstrainedShortestPathOnDagTest, MinusInfWeight) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, -kInf, {0.0}}}; + + EXPECT_DEATH(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{0.0}), + "-inf"); +} + +TEST(ConstrainedShortestPathOnDagTest, NaNWeight) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, std::numeric_limits::quiet_NaN(), {0.0}}}; + + EXPECT_DEATH(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{0.0}), + "NaN"); +} + +TEST(ConstrainedShortestPathOnDagTest, InfResource) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, 0.0, {kInf}}}; + + EXPECT_DEATH(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{0.0}), + "inf"); +} + +TEST(ConstrainedShortestPathOnDagTest, NegativeMaxResource) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length_and_resources = + {{source, destination, 0.0, {0.0}}}; + + EXPECT_DEATH(ConstrainedShortestPathsOnDag( + num_nodes, arcs_with_length_and_resources, source, + destination, /*max_resources=*/{-1.0}), + "negative"); +} + +TEST(ConstrainedShortestPathOnDagTest, SourceIsDestination) { + const int source = 0; + const int num_nodes = 1; + + EXPECT_DEATH( + ConstrainedShortestPathsOnDag( + num_nodes, /*arcs_with_length_and_resources=*/{}, source, source, + /*max_resources=*/{0.0}), + "source and destination"); +} + +TEST(ConstrainedShortestPathsOnDagWrapperTest, 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; + std::vector> arc_resources(1); + graph.AddArc(source, destination); + arc_lengths.push_back(1.0); + arc_resources[0].push_back({1.0}); + const std::vector topological_order = {source}; + const std::vector sources = {source}; + const std::vector destinations = {destination}; + const std::vector max_resources = {0.0}; + + EXPECT_DEATH(ConstrainedShortestPathsOnDagWrapper>( + &graph, &arc_lengths, &arc_resources, topological_order, + sources, destinations, &max_resources), + "Invalid topological order"); +} +#endif // NDEBUG + +} // namespace +} // namespace operations_research diff --git a/ortools/graph/dag_shortest_path.cc b/ortools/graph/dag_shortest_path.cc new file mode 100644 index 0000000000..f8c15379b4 --- /dev/null +++ b/ortools/graph/dag_shortest_path.cc @@ -0,0 +1,136 @@ +// 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. + +#include "ortools/graph/dag_shortest_path.h" + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/topologicalsorter.h" + +namespace operations_research { + +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()); + for (const auto& arc : arcs_with_length) { + graph.AddArc(arc.from, arc.to); + arc_lengths.push_back(arc.length); + } + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &arc_lengths); + + std::vector original_arc_indices(permutation.size()); + if (!permutation.empty()) { + for (ArcIndex i = 0; i < permutation.size(); ++i) { + original_arc_indices[permutation[i]] = i; + } + } + + absl::StatusOr> topological_order = + util::graph::FastTopologicalSort(graph); + CHECK_OK(topological_order) << "arcs_with_length form a cycle."; + + return ShortestPathOnDagProblem{ + .graph = std::move(graph), + .arc_lengths = std::move(arc_lengths), + .original_arc_indices = std::move(original_arc_indices), + .topological_order = std::move(topological_order).value()}; +} + +void GetOriginalArcPath(absl::Span 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()}; + } + + std::vector arc_path = shortest_path_on_dag.ArcPathTo(destination); + GetOriginalArcPath(problem.original_arc_indices, arc_path); + return PathWithLength{ + .length = shortest_path_on_dag.LengthTo(destination), + .arc_path = std::move(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 new file mode 100644 index 0000000000..91d11bd8d9 --- /dev/null +++ b/ortools/graph/dag_shortest_path.h @@ -0,0 +1,714 @@ +// 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 OR_TOOLS_GRAPH_DAG_SHORTEST_PATH_H_ +#define OR_TOOLS_GRAPH_DAG_SHORTEST_PATH_H_ + +#include +#if __cplusplus >= 202002L +#include +#endif +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" + +namespace operations_research { +// 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). +// +// In the DAG, multiple arcs between the same pair of nodes is allowed. However, +// self-loop arcs are not allowed. +// +// Note that we use the length formalism here, but the arc lengths can represent +// any numeric physical quantity. A shortest path will just be a path minimizing +// this quantity where the length of a path is the sum of the length of its +// arcs. An arc length can be negative, or +inf (indicating that it should not +// be used). An arc length cannot be -inf or nan. + +// ----------------------------------------------------------------------------- +// Basic API. +// ----------------------------------------------------------------------------- + +// `from` and `to` should both be in [0, num_nodes). +// If the length is +inf, then the arc should not be used. +struct ArcWithLength { + int from = 0; + int to = 0; + double length = 0.0; +}; + +struct PathWithLength { + double length = 0.0; + // The returned arc indices points into the `arcs_with_length` passed to the + // function below. + std::vector arc_path; + 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. +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. 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; + 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 `RunShortestPathOnDag()` 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. + ShortestPathsOnDagWrapper(const GraphType* graph, + const std::vector* arc_lengths, + absl::Span topological_order); + + // Computes the shortest path to all reachable nodes from the given sources. + // This must be called before any of the query functions below. + void RunShortestPathOnDag(absl::Span sources); + + // 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; + const std::vector& reached_nodes() const { return reached_nodes_; } + + // Returns the length of the shortest path from `node`'s source to `node`. + double LengthTo(NodeIndex node) const { return length_from_sources_[node]; } + std::vector LengthTo() const { return length_from_sources_; } + + // Returns the list of all the arcs in the shortest path from `node`'s + // source to `node`. CHECKs if the node is reachable. + std::vector ArcPathTo(NodeIndex node) const; + + // Returns the list of all the nodes in the shortest path from `node`'s + // source to `node` (including the source). CHECKs if the node is reachable. + std::vector NodePathTo(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_; } + + private: + static constexpr double kInf = std::numeric_limits::infinity(); + const GraphType* const graph_; + const std::vector* const arc_lengths_; + absl::Span const topological_order_; + + // Data about the last call of the RunShortestPathOnDag() function. + std::vector length_from_sources_; + std::vector incoming_shortest_path_arc_; + std::vector reached_nodes_; +}; + +// 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); + +// ----------------------------------------------------------------------------- +// Implementations. +// ----------------------------------------------------------------------------- +// 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 +#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)); + } + node_path.push_back(graph.Head(arc_path.back())); + return node_path; +} + +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) { + using NodeIndex = typename GraphType::NodeIndex; + using ArcIndex = typename GraphType::ArcIndex; + const NodeIndex num_nodes = graph.num_nodes(); + if (topological_order.size() != num_nodes) { + return absl::InvalidArgumentError(absl::StrFormat( + "topological_order.size() = %i, != graph.num_nodes() = %i", + topological_order.size(), num_nodes)); + } + std::vector inverse_topology(num_nodes, -1); + for (NodeIndex node = 0; node < topological_order.size(); ++node) { + if (inverse_topology[topological_order[node]] >= 0) { + return absl::InvalidArgumentError( + absl::StrFormat("node % i appears twice in topological order", + topological_order[node])); + } + inverse_topology[topological_order[node]] = node; + } + for (NodeIndex tail = 0; tail < num_nodes; ++tail) { + for (const ArcIndex arc : graph.OutgoingArcs(tail)) { + const NodeIndex head = graph.Head(arc); + if (inverse_topology[tail] >= inverse_topology[head]) { + return absl::InvalidArgumentError(absl::StrFormat( + "arc (%i, %i) is inconsistent with topological order", tail, head)); + } + } + } + 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(); + 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.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 new file mode 100644 index 0000000000..3ae65801e5 --- /dev/null +++ b/ortools/graph/dag_shortest_path_test.cc @@ -0,0 +1,1158 @@ +// 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. + +#include "ortools/graph/dag_shortest_path.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/log/check.h" +#include "absl/random/random.h" +#include "absl/status/status.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/dump_vars.h" +#include "ortools/base/gmock.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/graph_io.h" +#include "ortools/util/flat_matrix.h" + +namespace operations_research { +namespace { + +constexpr double kInf = std::numeric_limits::infinity(); + +using ::testing::ElementsAre; +using ::testing::FieldsAre; +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; + const int a = 2; + const int b = 3; + const int num_nodes = 4; + const std::vector arcs_with_length = {{source, a, 5.0}, + {source, b, 2.0}, + {a, destination, 3.0}, + {b, destination, 20.0}}; + + EXPECT_THAT( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*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; + 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( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/2.0, /*arc_path=*/ElementsAre(3, 2, 5), + /*node_path=*/ElementsAre(source, a, b, destination))); +} + +TEST(ShortestPathOnDagTest, 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( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*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; + const int a = 2; + const int num_nodes = 3; + const std::vector arcs_with_length = {{a, destination, 1.0}, + {source, a, kInf}}; + + EXPECT_THAT( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/kInf, /*arc_path=*/IsEmpty(), + /*node_path=*/IsEmpty())); +} + +TEST(ShortestPathOnDagTest, 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( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/4.0, /*arc_path=*/ElementsAre(3, 1), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ShortestPathOnDagTest, 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( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/1.0, /*arc_path=*/ElementsAre(0), + /*node_path=*/ElementsAre(source, destination))); +} + +TEST(ShortestPathOnDagTest, 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, 2.0}}; + + EXPECT_THAT( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/2.0, /*arc_path=*/ElementsAre(1), + /*node_path=*/ElementsAre(source, destination))); +} + +TEST(ShortestPathOnDagTest, 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( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/8.0, /*arc_path=*/ElementsAre(0, 2), + /*node_path=*/ElementsAre(source, a, destination))); + + // Update the length of arc b -> destination from 20.0 to -1.0. + arcs_with_length[3].length = -1.0; + + EXPECT_THAT( + ShortestPathsOnDag(num_nodes, arcs_with_length, source, destination), + FieldsAre(/*length=*/1.0, /*arc_path=*/ElementsAre(1, 3), + /*node_path=*/ElementsAre(source, b, destination))); +} + +TEST(ShortestPathsOnDagWrapperTest, 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}; + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + shortest_path_on_dag.RunShortestPathOnDag({source_1, source_2}); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination), -6.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination), ElementsAre(0)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination), + ElementsAre(source_1, destination)); +} + +TEST(ShortestPathsOnDagWrapperTest, 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=*/2); + 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); + const std::vector topological_order = {source_1, source_2, destination}; + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + shortest_path_on_dag.RunShortestPathOnDag({source_1, source_2}); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination), -4.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination), ElementsAre(0, 1)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination), + ElementsAre(source_1, source_2, destination)); +} + +TEST(ShortestPathsOnDagWrapperTest, 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}; + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + shortest_path_on_dag.RunShortestPathOnDag({source}); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination_1)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination_1), 3.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination_1), ElementsAre(0)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination_1), + ElementsAre(source, destination_1)); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination_2)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination_2), 1.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination_2), ElementsAre(1)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination_2), + ElementsAre(source, destination_2)); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination_3)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination_3), 2.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination_3), ElementsAre(2)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination_3), + ElementsAre(source, destination_3)); +} + +TEST(ShortestPathsOnDagWrapperTest, UpdateCost) { + const int source = 0; + const int destination = 1; + const int a = 2; + const int b = 3; + const int num_nodes = 4; + util::ListGraph<> graph(num_nodes, /*arc_capacity=*/4); + std::vector arc_lengths; + graph.AddArc(source, a); + arc_lengths.push_back(5.0); + graph.AddArc(source, b); + arc_lengths.push_back(2.0); + graph.AddArc(a, destination); + arc_lengths.push_back(3.0); + graph.AddArc(b, destination); + arc_lengths.push_back(20.0); + const std::vector topological_order = {source, a, b, destination}; + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + shortest_path_on_dag.RunShortestPathOnDag({source}); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination), 8.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination), ElementsAre(0, 2)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination), + ElementsAre(source, a, destination)); + + // Update the length of arc b -> destination from 20.0 to -1.0. + arc_lengths[3] = -1.0; + shortest_path_on_dag.RunShortestPathOnDag({source}); + + EXPECT_TRUE(shortest_path_on_dag.IsReachable(destination)); + EXPECT_THAT(shortest_path_on_dag.LengthTo(destination), 1.0); + EXPECT_THAT(shortest_path_on_dag.ArcPathTo(destination), ElementsAre(1, 3)); + EXPECT_THAT(shortest_path_on_dag.NodePathTo(destination), + ElementsAre(source, b, destination)); +} + +// 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. +std::pair, std::vector> BuildRandomDag( + const int64_t num_nodes, const int64_t num_arcs) { + absl::BitGen bit_gen; + CHECK_GE(num_nodes, 2); + CHECK_GE(num_arcs, 1); + CHECK_LE(num_arcs, (num_nodes * (num_nodes - 1)) / 2); + std::vector topological_order(num_nodes); + topological_order.back() = num_nodes - 1; + absl::Span non_start_end = + absl::MakeSpan(topological_order).subspan(1, num_nodes - 2); + absl::c_iota(non_start_end, 1); + absl::c_shuffle(non_start_end, bit_gen); + int edges_added = 0; + util::StaticGraph<> graph(num_nodes, num_arcs); + graph.AddArc(0, num_nodes - 1); + while (edges_added < num_arcs - 1) { + int start_index = absl::Uniform(bit_gen, 0, num_nodes - 1); + int end_index = absl::Uniform(bit_gen, start_index + 1, num_nodes); + graph.AddArc(topological_order[start_index], topological_order[end_index]); + edges_added++; + } + graph.Build(); + return {graph, topological_order}; +} + +// The length of each arc is drawn uniformly at random within a given interval +// except if the first arc from 0 to num_nodes-1 where it is set to a large +// length. +std::vector GenerateRandomLengths(const util::StaticGraph<>& graph, + const double min_length = 0.0, + const double max_length = 10.0, + const double large_length = 10000.0) { + absl::BitGen bit_gen; + std::vector arc_lengths; + arc_lengths.reserve(graph.num_arcs()); + bool large_length_set = false; + for (util::StaticGraph<>::ArcIndex arc = 0; arc < graph.num_arcs(); ++arc) { + if (!large_length_set && graph.Tail(arc) == 0 && + graph.Head(arc) == graph.num_nodes() - 1) { + arc_lengths.push_back(large_length); + large_length_set = true; + continue; + } + arc_lengths.push_back(static_cast( + absl::Uniform(bit_gen, min_length, max_length))); + } + return arc_lengths; +} + +TEST(ShortestPathsOnDagWrapperTest, 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); + + // Run Floyd-Warshall as a 'reference' shortest path algorithm. + FlatMatrix ref_dist(num_nodes, num_nodes, kInf); + for (int a = 0; a < num_arcs; ++a) { + double& d = ref_dist[graph.Tail(a)][graph.Head(a)]; + if (arc_lengths[a] < d) d = arc_lengths[a]; + } + for (int node = 0; node < num_nodes; ++node) { + ref_dist[node][node] = 0; + } + for (int k = 0; k < num_nodes; ++k) { + for (int i = 0; i < num_nodes; ++i) { + for (int j = 0; j < num_nodes; ++j) { + const double dist_through_k = ref_dist[i][k] + ref_dist[k][j]; + if (dist_through_k < ref_dist[i][j]) ref_dist[i][j] = dist_through_k; + } + } + } + + // Now, run some shortest paths and verify that they match. To balance out + // the FW (Floyd-Warshall) which is O(N³), we run more than one shortest + // path per FW. + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + 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); + } + // Precompute the reference minimum distance to each node (using any of + // the sources), and the expected reached nodes: any node whose distance + // is < kInf. + std::vector node_min_dist(num_nodes, kInf); + std::vector expected_reached_nodes; + for (int node = 0; node < num_nodes; ++node) { + double min_dist = kInf; + for (const int source : sources) { + min_dist = std::min(min_dist, ref_dist[source][node]); + } + node_min_dist[node] = min_dist; + if (min_dist < kInf) expected_reached_nodes.push_back(node); + } + shortest_path_on_dag.RunShortestPathOnDag(sources); + for (const int node : expected_reached_nodes) { + EXPECT_TRUE(shortest_path_on_dag.IsReachable(node)); + EXPECT_EQ(shortest_path_on_dag.LengthTo(node), node_min_dist[node]) + << 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(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 + +// ----------------------------------------------------------------------------- +// ShortestPathsOnDagWrapper benchmarks. +// ----------------------------------------------------------------------------- +void BM_RandomDag(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 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(); + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + for (auto _ : state) { + // Pick a arc lengths scenario at random. + arc_lengths = + arc_lengths_scenarios[absl::Uniform(bit_gen, 0, num_scenarios)]; + shortest_path_on_dag.RunShortestPathOnDag({0}); + CHECK(shortest_path_on_dag.IsReachable(num_nodes - 1)); + const double minimum_length = shortest_path_on_dag.LengthTo(num_nodes - 1); + CHECK_GE(minimum_length, 0.0); + CHECK_LE(minimum_length, 10000.0); + } + state.SetItemsProcessed(state.iterations() * (num_nodes + num_arcs)); +} + +BENCHMARK(BM_RandomDag) + ->ArgPair(1000, 10) + ->ArgPair(1 << 16, 4) + ->ArgPair(1 << 16, 16) + ->ArgPair(1 << 22, 4) + ->ArgPair(1 << 22, 16); + +void BM_LineDag(benchmark::State& state) { + const int num_nodes = state.range(0); + const int num_edges = num_nodes - 1; + std::vector topological_order(num_nodes); + util::StaticGraph<> graph(num_nodes, num_edges); + absl::c_iota(topological_order, 0); + for (int i = 0; i < num_nodes - 1; ++i) { + graph.AddArc(i, i + 1); + } + graph.Build(); + std::vector arc_lengths(num_edges, 1); + ShortestPathsOnDagWrapper> shortest_path_on_dag( + &graph, &arc_lengths, topological_order); + for (auto _ : state) { + shortest_path_on_dag.RunShortestPathOnDag({0}); + CHECK(shortest_path_on_dag.IsReachable(num_nodes - 1)); + CHECK_EQ(shortest_path_on_dag.LengthTo(num_nodes - 1), num_nodes - 1); + CHECK_EQ(shortest_path_on_dag.ArcPathTo(num_nodes - 1).size(), + num_nodes - 1); + CHECK_EQ(shortest_path_on_dag.NodePathTo(num_nodes - 1).size(), num_nodes); + } + state.SetItemsProcessed(state.iterations() * num_nodes); +} + +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, HeapSizeMustBeLargerThanPathCount) { + const int source = 0; + const int destination = 1; + const int num_nodes = 2; + const std::vector arcs_with_length = { + {source, destination, 2.0}, + {source, destination, 3.0}, + {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(2), + /*node_path=*/ElementsAre(source, destination)), + FieldsAre( + /*length=*/2.0, /*arc_path=*/ElementsAre(0), + /*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 diff --git a/ortools/graph/samples/BUILD.bazel b/ortools/graph/samples/BUILD.bazel index 103dd7dd57..fcfde6be98 100644 --- a/ortools/graph/samples/BUILD.bazel +++ b/ortools/graph/samples/BUILD.bazel @@ -31,6 +31,22 @@ code_sample_cc(name = "bfs_one_to_all") code_sample_cc(name = "bfs_undirected") +code_sample_cc(name = "dag_shortest_path_one_to_all") + +code_sample_cc(name = "dag_shortest_path_sequential") + +code_sample_cc(name = "dag_simple_shortest_path") + +code_sample_cc(name = "dag_multiple_shortest_paths_one_to_all") + +code_sample_cc(name = "dag_multiple_shortest_paths_sequential") + +code_sample_cc(name = "dag_simple_multiple_shortest_paths") + +code_sample_cc(name = "dag_constrained_shortest_path_sequential") + +code_sample_cc(name = "dag_simple_constrained_shortest_path") + code_sample_cc(name = "dijkstra_all_pairs_shortest_paths") code_sample_cc(name = "dijkstra_directed") diff --git a/ortools/graph/samples/code_samples.bzl b/ortools/graph/samples/code_samples.bzl index 1311db9c52..0fdd55f0ea 100644 --- a/ortools/graph/samples/code_samples.bzl +++ b/ortools/graph/samples/code_samples.bzl @@ -29,6 +29,8 @@ def code_sample_cc(name): "//ortools/graph:assignment", "//ortools/graph:bounded_dijkstra", "//ortools/graph:bfs", + "//ortools/graph:dag_constrained_shortest_path", + "//ortools/graph:dag_shortest_path", "//ortools/graph:linear_assignment", "//ortools/graph:max_flow", "//ortools/graph:min_cost_flow", @@ -49,6 +51,8 @@ def code_sample_cc(name): "//ortools/graph:assignment", "//ortools/graph:bounded_dijkstra", "//ortools/graph:bfs", + "//ortools/graph:dag_constrained_shortest_path", + "//ortools/graph:dag_shortest_path", "//ortools/graph:linear_assignment", "//ortools/graph:max_flow", "//ortools/graph:min_cost_flow", diff --git a/ortools/graph/samples/dag_constrained_shortest_path_sequential.cc b/ortools/graph/samples/dag_constrained_shortest_path_sequential.cc new file mode 100644 index 0000000000..bc09f3f36f --- /dev/null +++ b/ortools/graph/samples/dag_constrained_shortest_path_sequential.cc @@ -0,0 +1,139 @@ +// 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. + +// [START imports] +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_constrained_shortest_path.h" +#include "ortools/graph/graph.h" +// [END imports] + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // [START graph] + // Create a graph with n + 2 nodes, indexed from 0: + // * Node n is `source` + // * Node n+1 is `dest` + // * Nodes M = [0, 1, ..., n-1] are in the middle. + // + // There is a single resource constraints with limit 1. + // + // The graph has 3 * n - 1 arcs (with weights and both resources): + // * (source -> i) with weight 100 and no resource use for i in M + // * (i -> dest) with weight 100 and no resource use for i in M + // * (i -> (i+1)) with weight 1 and resource use of 1 for i = 0, ..., n-2 + // + // Every path [source, i, dest] for i in M is a constrained shortest path from + // source to dest with weight 200. + const int n = 5; + const int source = n; + const int dest = n + 1; + const int num_arcs = 3 * n - 1; + util::StaticGraph<> graph; + // There are 3 types of arcs: (1) source to M, (2) M to dest, and (3) within + // M. This vector stores all of them, first of type (1), then type (2), + // then type (3). The arcs are ordered by i in M within each type. + std::vector weights(num_arcs); + // Resources are first indexed by resource, then by arc. + std::vector> resources(1, std::vector(num_arcs)); + + for (int i = 0; i < n; ++i) { + graph.AddArc(source, i); + weights[i] = 100.0; + resources[0][i] = 0.0; + } + for (int i = 0; i < n; ++i) { + graph.AddArc(i, dest); + weights[n + i] = 100.0; + resources[0][n + i] = 0.0; + } + for (int i = 0; i + 1 < n; ++i) { + graph.AddArc(i, i + 1); + weights[2 * n + i] = 1.0; + resources[0][2 * n + i] = 1.0; + } + + // Static graph reorders the arcs at Build() time, use permutation to get from + // the old ordering to the new one. + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &weights); + util::Permute(permutation, &resources[0]); + // [END graph] + + // [START first-path] + // A reusable shortest path calculator. + // We need a topological order. For this structured graph, we find it by hand + // instead of using util::graph::FastTopologicalSort(). + std::vector topological_order = {source}; + for (int32_t i = 0; i < n; ++i) { + topological_order.push_back(i); + } + topological_order.push_back(dest); + + const std::vector sources = {source}; + const std::vector destinations = {dest}; + const std::vector max_resources = {1.0}; + + operations_research::ConstrainedShortestPathsOnDagWrapper> + constrained_shortest_path_on_dag(&graph, &weights, &resources, + topological_order, sources, destinations, + &max_resources); + operations_research::GraphPathWithLength> + initial_constrained_shortest_path = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + + std::cout << "Initial distance: " << initial_constrained_shortest_path.length + << std::endl; + std::cout << "Initial path: " + << absl::StrJoin(initial_constrained_shortest_path.node_path, ", ") + << std::endl; + // [END first-path] + + // [START more-paths] + // Now, we make a single arc from source to M free, and a single arc from M + // to dest free, and resolve. If the free edge from the source hits before + // the free edge to the dest in M, we use both, walking through M. Otherwise, + // we use only one free arc. + std::vector> fast_paths = {{2, 3}, {8, 1}, {3, 7}}; + for (const auto [free_from_source, free_to_dest] : fast_paths) { + weights[permutation[free_from_source]] = 0; + weights[permutation[n + free_to_dest]] = 0; + + operations_research::GraphPathWithLength> + constrained_shortest_path = + constrained_shortest_path_on_dag.RunConstrainedShortestPathOnDag(); + std::cout << "source -> " << free_from_source << " and " << free_to_dest + << " -> dest are now free" << std::endl; + std::string label = absl::StrCat("_", free_from_source, "_", free_to_dest); + std::cout << "Distance" << label << ": " << constrained_shortest_path.length + << std::endl; + std::cout << "Path" << label << ": " + << absl::StrJoin(constrained_shortest_path.node_path, ", ") + << std::endl; + + // Restore the old weights + weights[permutation[free_from_source]] = 100; + weights[permutation[n + free_to_dest]] = 100; + } + // [END more-paths] + return 0; +} diff --git a/ortools/graph/samples/dag_multiple_shortest_paths_one_to_all.cc b/ortools/graph/samples/dag_multiple_shortest_paths_one_to_all.cc new file mode 100644 index 0000000000..7f51c3f1e4 --- /dev/null +++ b/ortools/graph/samples/dag_multiple_shortest_paths_one_to_all.cc @@ -0,0 +1,87 @@ +// 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. + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_macros.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/topologicalsorter.h" + +namespace { + +absl::Status Main() { + util::StaticGraph<> graph; + std::vector weights; + graph.AddArc(0, 1); + weights.push_back(2.0); + graph.AddArc(0, 2); + weights.push_back(5.0); + graph.AddArc(1, 4); + weights.push_back(1.0); + graph.AddArc(2, 4); + weights.push_back(-3.0); + graph.AddArc(3, 4); + weights.push_back(0.0); + + // Static graph reorders the arcs at Build() time, use permutation to get + // from the old ordering to the new one. + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &weights); + + // We need a topological order. We can find it by hand on this small graph, + // e.g., {0, 1, 2, 3, 4}, but we demonstrate how to compute one instead. + ASSIGN_OR_RETURN(const std::vector topological_order, + util::graph::FastTopologicalSort(graph)); + + operations_research::KShortestPathsOnDagWrapper> + shortest_paths_on_dag(&graph, &weights, topological_order, + /*path_count=*/2); + const int source = 0; + shortest_paths_on_dag.RunKShortestPathOnDag({source}); + + // For each node other than 0, print its distance and the shortest path. + for (int node = 1; node < 5; ++node) { + std::cout << "Node " << node << ":\n"; + if (!shortest_paths_on_dag.IsReachable(node)) { + std::cout << "\tNo path to node " << node << std::endl; + continue; + } + const std::vector lengths = shortest_paths_on_dag.LengthsTo(node); + const std::vector> paths = + shortest_paths_on_dag.NodePathsTo(node); + for (int path_index = 0; path_index < lengths.size(); ++path_index) { + std::cout << "\t#" << (path_index + 1) << " shortest path to node " + << node << " has length: " << lengths[path_index] << std::endl; + std::cout << "\t#" << (path_index + 1) << " shortest path to node " + << node << " is: " << absl::StrJoin(paths[path_index], ", ") + << std::endl; + } + } + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + QCHECK_OK(Main()); + return 0; +} diff --git a/ortools/graph/samples/dag_multiple_shortest_paths_sequential.cc b/ortools/graph/samples/dag_multiple_shortest_paths_sequential.cc new file mode 100644 index 0000000000..4538eae34f --- /dev/null +++ b/ortools/graph/samples/dag_multiple_shortest_paths_sequential.cc @@ -0,0 +1,135 @@ +// 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. + +// [START imports] +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" +// [END imports] + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // [START graph] + // Create a graph with n + 2 nodes, indexed from 0: + // * Node n is `source` + // * Node n+1 is `dest` + // * Nodes M = [0, 1, ..., n-1] are in the middle. + // + // The graph has 3 * n - 1 arcs (with weights): + // * (source -> i) with weight 100 + i for i in M + // * (i -> dest) with weight 100 + i for i in M + // * (i -> (i+1)) with weight 10 for i = 0, ..., n-2 + const int n = 10; + const int source = n; + const int dest = n + 1; + util::StaticGraph<> graph; + // There are 3 types of arcs: (1) source to M, (2) M to dest, and (3) within + // M. This vector stores all of them, first of type (1), then type (2), + // then type (3). The arcs are ordered by i in M within each type. + std::vector weights(3 * n - 1); + + for (int i = 0; i < n; ++i) { + graph.AddArc(source, i); + weights[i] = 100.0 + i; + } + for (int i = 0; i < n; ++i) { + graph.AddArc(i, dest); + weights[n + i] = 100.0 + i; + } + for (int i = 0; i + 1 < n; ++i) { + graph.AddArc(i, i + 1); + weights[2 * n + i] = 10.0; + } + + // Static graph reorders the arcs at Build() time, use permutation to get from + // the old ordering to the new one. + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &weights); + // [END graph] + + // [START first-path] + // A reusable shortest path calculator. + // We need a topological order. For this structured graph, we find it by hand + // instead of using util::graph::FastTopologicalSort(). + std::vector topological_order = {source}; + for (int32_t i = 0; i < n; ++i) { + topological_order.push_back(i); + } + topological_order.push_back(dest); + + operations_research::KShortestPathsOnDagWrapper> + shortest_paths_on_dag(&graph, &weights, topological_order, + /*path_count=*/2); + shortest_paths_on_dag.RunKShortestPathOnDag({source}); + + const std::vector initial_lengths = + shortest_paths_on_dag.LengthsTo(dest); + const std::vector> initial_paths = + shortest_paths_on_dag.NodePathsTo(dest); + + std::cout << "No free arcs" << std::endl; + for (int path_index = 0; path_index < initial_lengths.size(); ++path_index) { + std::cout << "\t#" << (path_index + 1) + << " shortest path has length: " << initial_lengths[path_index] + << std::endl; + std::cout << "\t#" << (path_index + 1) << " shortest path is: " + << absl::StrJoin(initial_paths[path_index], ", ") << std::endl; + } + // [END first-path] + + // [START more-paths] + // Now, we make a single arc from source to M free, and a single arc from M + // to dest free, and resolve. If the free edge from the source hits before + // the free edge to the dest in M, we use both, walking through M. Otherwise, + // we use only one free arc. + std::vector> fast_paths = { + {2, 4}, {8, 1}, {3, 3}, {0, 0}}; + for (const auto [free_from_source, free_to_dest] : fast_paths) { + weights[permutation[free_from_source]] = 0; + weights[permutation[n + free_to_dest]] = 0; + + shortest_paths_on_dag.RunKShortestPathOnDag({source}); + std::cout << "source -> " << free_from_source << " and " << free_to_dest + << " -> dest are now free" << std::endl; + std::string label = + absl::StrCat(" (", free_from_source, ", ", free_to_dest, ")"); + + const std::vector lengths = shortest_paths_on_dag.LengthsTo(dest); + const std::vector> paths = + shortest_paths_on_dag.NodePathsTo(dest); + + for (int path_index = 0; path_index < lengths.size(); ++path_index) { + std::cout << "\t#" << (path_index + 1) << " shortest path" << label + << " has length: " << lengths[path_index] << std::endl; + std::cout << "\t#" << (path_index + 1) << " shortest path" << label + << " is: " << absl::StrJoin(paths[path_index], ", ") + << std::endl; + } + + // Restore the old weights + weights[permutation[free_from_source]] = 100 + free_from_source; + weights[permutation[n + free_to_dest]] = 100 + free_to_dest; + } + // [END more-paths] + return 0; +} diff --git a/ortools/graph/samples/dag_shortest_path_one_to_all.cc b/ortools/graph/samples/dag_shortest_path_one_to_all.cc new file mode 100644 index 0000000000..118f31e221 --- /dev/null +++ b/ortools/graph/samples/dag_shortest_path_one_to_all.cc @@ -0,0 +1,81 @@ +// 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. + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_macros.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" +#include "ortools/graph/topologicalsorter.h" + +namespace { + +absl::Status Main() { + util::StaticGraph<> graph; + std::vector weights; + graph.AddArc(0, 2); + weights.push_back(5.0); + graph.AddArc(0, 3); + weights.push_back(4.0); + graph.AddArc(1, 3); + weights.push_back(1.0); + graph.AddArc(2, 4); + weights.push_back(-3.0); + graph.AddArc(3, 4); + weights.push_back(0.0); + + // Static graph reorders the arcs at Build() time, use permutation to get + // from the old ordering to the new one. + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &weights); + + // We need a topological order. We can find it by hand on this small graph, + // e.g., {0, 1, 2, 3, 4}, but we demonstrate how to compute one instead. + ASSIGN_OR_RETURN(const std::vector topological_order, + util::graph::FastTopologicalSort(graph)); + + operations_research::ShortestPathsOnDagWrapper> + shortest_path_on_dag(&graph, &weights, topological_order); + const int source = 0; + shortest_path_on_dag.RunShortestPathOnDag({source}); + + // For each node other than 0, print its distance and the shortest path. + for (int i = 1; i < 5; ++i) { + if (shortest_path_on_dag.IsReachable(i)) { + std::cout << "Length of shortest path to node " << i << ": " + << shortest_path_on_dag.LengthTo(i) << std::endl; + std::cout << "Shortest path to node " << i << ": " + << absl::StrJoin(shortest_path_on_dag.NodePathTo(i), ", ") + << std::endl; + + } else { + std::cout << "No path to node: " << i << std::endl; + } + } + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + QCHECK_OK(Main()); + return 0; +} diff --git a/ortools/graph/samples/dag_shortest_path_sequential.cc b/ortools/graph/samples/dag_shortest_path_sequential.cc new file mode 100644 index 0000000000..11b266d090 --- /dev/null +++ b/ortools/graph/samples/dag_shortest_path_sequential.cc @@ -0,0 +1,120 @@ +// 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. + +// [START imports] +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_shortest_path.h" +#include "ortools/graph/graph.h" +// [END imports] + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // [START graph] + // Create a graph with n + 2 nodes, indexed from 0: + // * Node n is `source` + // * Node n+1 is `dest` + // * Nodes M = [0, 1, ..., n-1] are in the middle. + // + // The graph has 3 * n - 1 arcs (with weights): + // * (source -> i) with weight 100 for i in M + // * (i -> dest) with weight 100 for i in M + // * (i -> (i+1)) with weight 1 for i = 0, ..., n-2 + // + // Every path [source, i, dest] for i in M is a shortest path from source to + // dest with weight 200. + const int n = 10; + const int source = n; + const int dest = n + 1; + util::StaticGraph<> graph; + // There are 3 types of arcs: (1) source to M, (2) M to dest, and (3) within + // M. This vector stores all of them, first of type (1), then type (2), + // then type (3). The arcs are ordered by i in M within each type. + std::vector weights(3 * n - 1); + + for (int i = 0; i < n; ++i) { + graph.AddArc(source, i); + weights[i] = 100.0; + } + for (int i = 0; i < n; ++i) { + graph.AddArc(i, dest); + weights[n + i] = 100.0; + } + for (int i = 0; i + 1 < n; ++i) { + graph.AddArc(i, i + 1); + weights[2 * n + i] = 1.0; + } + + // Static graph reorders the arcs at Build() time, use permutation to get from + // the old ordering to the new one. + std::vector permutation; + graph.Build(&permutation); + util::Permute(permutation, &weights); + // [END graph] + + // [START first-path] + // A reusable shortest path calculator. + // We need a topological order. For this structured graph, we find it by hand + // instead of using util::graph::FastTopologicalSort(). + std::vector topological_order = {source}; + for (int i = 0; i < n; ++i) { + topological_order.push_back(i); + } + topological_order.push_back(dest); + + operations_research::ShortestPathsOnDagWrapper> + shortest_path_on_dag(&graph, &weights, topological_order); + shortest_path_on_dag.RunShortestPathOnDag({source}); + + std::cout << "Initial distance: " << shortest_path_on_dag.LengthTo(dest) + << std::endl; + std::cout << "Initial path: " + << absl::StrJoin(shortest_path_on_dag.NodePathTo(dest), ", ") + << std::endl; + // [END first-path] + + // [START more-paths] + // Now, we make a single arc from source to M free, and a single arc from M + // to dest free, and resolve. If the free edge from the source hits before + // the free edge to the dest in M, we use both, walking through M. Otherwise, + // we use only one free arc. + std::vector> fast_paths = {{2, 4}, {8, 1}, {3, 7}}; + for (const auto [free_from_source, free_to_dest] : fast_paths) { + weights[permutation[free_from_source]] = 0; + weights[permutation[n + free_to_dest]] = 0; + + shortest_path_on_dag.RunShortestPathOnDag({source}); + std::cout << "source -> " << free_from_source << " and " << free_to_dest + << " -> dest are now free" << std::endl; + std::string label = absl::StrCat("_", free_from_source, "_", free_to_dest); + std::cout << "Distance" << label << ": " + << shortest_path_on_dag.LengthTo(dest) << std::endl; + std::cout << "Path" << label << ": " + << absl::StrJoin(shortest_path_on_dag.NodePathTo(dest), ", ") + << std::endl; + + // Restore the old weights + weights[permutation[free_from_source]] = 100; + weights[permutation[n + free_to_dest]] = 100; + } + // [END more-paths] + return 0; +} diff --git a/ortools/graph/samples/dag_simple_constrained_shortest_path.cc b/ortools/graph/samples/dag_simple_constrained_shortest_path.cc new file mode 100644 index 0000000000..1b4592c51e --- /dev/null +++ b/ortools/graph/samples/dag_simple_constrained_shortest_path.cc @@ -0,0 +1,47 @@ +// 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. + +#include +#include + +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_constrained_shortest_path.h" +#include "ortools/graph/dag_shortest_path.h" + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // The input graph, encoded as a list of arcs with distances. + std::vector arcs = { + {.from = 0, .to = 1, .length = 5, .resources = {1, 2}}, + {.from = 0, .to = 2, .length = 4, .resources = {3, 2}}, + {.from = 0, .to = 2, .length = 1, .resources = {2, 3}}, + {.from = 1, .to = 3, .length = -3, .resources = {8, 0}}, + {.from = 2, .to = 3, .length = 0, .resources = {3, 1}}}; + const int num_nodes = 4; + const std::vector max_resources = {6, 3}; + + const int source = 0; + const int destination = 3; + const operations_research::PathWithLength path_with_length = + operations_research::ConstrainedShortestPathsOnDag( + num_nodes, arcs, source, destination, max_resources); + + // Print to length of the path and then the nodes in the path. + std::cout << "Constrained shortest path length: " << path_with_length.length + << std::endl; + std::cout << "Constrained shortest path nodes: " + << absl::StrJoin(path_with_length.node_path, ", ") << std::endl; + return 0; +} diff --git a/ortools/graph/samples/dag_simple_multiple_shortest_paths.cc b/ortools/graph/samples/dag_simple_multiple_shortest_paths.cc new file mode 100644 index 0000000000..950f6cd3c9 --- /dev/null +++ b/ortools/graph/samples/dag_simple_multiple_shortest_paths.cc @@ -0,0 +1,47 @@ +// 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. + +#include +#include + +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_shortest_path.h" + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // The input graph, encoded as a list of arcs with distances. + std::vector arcs = { + {.from = 0, .to = 1, .length = 2}, {.from = 0, .to = 2, .length = 5}, + {.from = 0, .to = 3, .length = 4}, {.from = 1, .to = 4, .length = 1}, + {.from = 2, .to = 4, .length = -3}, {.from = 3, .to = 4, .length = 0}}; + const int num_nodes = 5; + + const int source = 0; + const int destination = 4; + const int path_count = 2; + const std::vector paths_with_length = + operations_research::KShortestPathsOnDag(num_nodes, arcs, source, + destination, path_count); + + for (int path_index = 0; path_index < paths_with_length.size(); + ++path_index) { + std::cout << "#" << (path_index + 1) << " shortest path has length: " + << paths_with_length[path_index].length << std::endl; + std::cout << "#" << (path_index + 1) << " shortest path is: " + << absl::StrJoin(paths_with_length[path_index].node_path, ", ") + << std::endl; + } + return 0; +} diff --git a/ortools/graph/samples/dag_simple_shortest_path.cc b/ortools/graph/samples/dag_simple_shortest_path.cc new file mode 100644 index 0000000000..cb7fb5b119 --- /dev/null +++ b/ortools/graph/samples/dag_simple_shortest_path.cc @@ -0,0 +1,44 @@ +// 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. + +#include +#include + +#include "absl/strings/str_join.h" +#include "ortools/base/init_google.h" +#include "ortools/graph/dag_shortest_path.h" + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // The input graph, encoded as a list of arcs with distances. + std::vector arcs = { + {.from = 0, .to = 2, .length = 5}, + {.from = 0, .to = 3, .length = 4}, + {.from = 1, .to = 3, .length = 1}, + {.from = 2, .to = 4, .length = -3}, + {.from = 3, .to = 4, .length = 0}}; + const int num_nodes = 5; + + const int source = 0; + const int destination = 4; + const operations_research::PathWithLength path_with_length = + operations_research::ShortestPathsOnDag(num_nodes, arcs, source, + destination); + + // Print to length of the path and then the nodes in the path. + std::cout << "Shortest path length: " << path_with_length.length << std::endl; + std::cout << "Shortest path nodes: " + << absl::StrJoin(path_with_length.node_path, ", ") << std::endl; + return 0; +}