Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
k_shortest_paths.h
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// Algorithms to compute k-shortest paths. Currently, only Yen's algorithm is
15// implemented.
16//
17// TODO(user): implement Lawler's modification:
18// https://pubsonline.informs.org/doi/abs/10.1287/mnsc.18.7.401
19//
20// | Algo. | Neg. weights | Neg.-weight loops | Graph type | Loopless paths |
21// |-------|--------------|-------------------|--------------|----------------|
22// | Yen | No | No | (Un)directed | Yes |
23//
24//
25// Loopless path: path not going through the same node more than once. Also
26// called simple path.
27//
28//
29// Design choices
30// ==============
31//
32// The design takes some inspiration from `shortest_paths.h` and
33// `bounded_dijkstra.h`, but the shortest-path and k-shortest-path problems have
34// vastly different structures.
35// For instance, a path container that only stores distances, like
36// `DistanceContainer` in `shortest_paths.h`, is irrelevant as an output for
37// this problem: it can only characterize one path, the shortest one.
38// This is why the results are stored in an intermediate structure, containing
39// the paths (as a sequence of nodes, just like `PathContainerImpl` subclasses)
40// and their distance.
41//
42// Only the one-to-one k-shortest-path problem is well-defined. Variants with
43// multiple sources and/or destinations pose representational challenges whose
44// solution is likely to be algorithm-dependent.
45// Optimizations of path storage such as `PathTree` are not general enough to
46// store k shortest paths: the set of paths for a given index for many
47// sources/destinations is not ensured to form a set for each index. (While the
48// first paths will form such a tree, storing *different* second paths for each
49// source-destination pair may be impossible to do in a tree.)
50//
51// Unlike the functions in `shortest_paths.h`, the functions in this file
52// directly return their result, to follow the current best practices.
53
54#ifndef OR_TOOLS_GRAPH_K_SHORTEST_PATHS_H_
55#define OR_TOOLS_GRAPH_K_SHORTEST_PATHS_H_
56
57#include <algorithm>
58#include <iterator>
59#include <limits>
60#include <queue>
61#include <tuple>
62#include <utility>
63#include <vector>
64
65#include "absl/algorithm/container.h"
66#include "absl/base/optimization.h"
67#include "absl/container/flat_hash_set.h"
68#include "absl/log/check.h"
69#include "absl/strings/str_join.h"
70#include "absl/types/span.h"
75
76namespace operations_research {
77
78// Stores the solution to a k-shortest path problem. `paths` contains up to `k`
79// paths from `source` to `destination` (these nodes are arguments to the
80// algorithm), each having a distance stored in `distances`.
81//
82// The paths in `paths` start with `origin` and end at `destination`.
83//
84// If the computations are unsuccessful for any reason, the vectors are empty.
85struct KShortestPaths {
86 // The paths are stored as vectors of nodes, like the other graph algorithms.
87 // TODO(user): what about vectors of arcs? That might be faster
88 // (potentially, add a function to transform it into a vector of nodes if the
89 // user really needs it). It would also have the nice benefit of removing the
90 // need for `distances` (compute it on the fly), with a reference to the graph
91 // and the costs.
92 std::vector<std::vector<NodeIndex>> paths;
93 std::vector<PathDistance> distances;
94};
95
96// Computes up to k shortest paths from the node `source` to the node
97// `destination` in the given directed `graph`. The paths are guaranteed not to
98// have loops.
99//
100// Hypotheses on input (which are not checked at runtime):
101// - No multigraphs (more than one edge or a pair of nodes). The behavior is
102// undefined otherwise.
103// - The `arc_lengths` are supposed to be nonnegative. The behavior is
104// undefined otherwise.
105// TODO(user): relax to "no negative-weight cycles" (no Dijkstra).
106// - The graphs might have loops.
107//
108// This function uses Yen's algorithm, which guarantees to find the first k
109// shortest paths in O(k n (m + n log n)) for n nodes and m edges. This
110// algorithm is an implementation of the idea of detours.
111//
112// Yen, Jin Y. "Finding the k Shortest Loopless Paths in a Network". Management
113// Science. 17 (11): 712–716, 1971.
114// https://doi.org/10.1287%2Fmnsc.17.11.712
115template <class GraphType>
116KShortestPaths YenKShortestPaths(const GraphType& graph,
117 const std::vector<PathDistance>& arc_lengths,
118 NodeIndex source, NodeIndex destination,
119 unsigned k);
120
121// End of the interface. Below is the implementation.
122
123// TODO(user): introduce an enum to choose the algorithm. It's useless as
124// long as this file only provides Yen.
125
126namespace internal {
127
128const PathDistance kMaxDistance = std::numeric_limits<PathDistance>::max() - 1;
130 std::numeric_limits<PathDistance>::max();
131
132// Determines the arc index from a source to a destination.
133//
134// This operation requires iterating through the set of outgoing arcs from the
135// source node, which might be expensive.
136//
137// In a multigraph, this function returns an index for one of the edges between
138// the source and the destination.
139template <class GraphType>
140ArcIndex FindArcIndex(const GraphType& graph, const NodeIndex source,
141 const NodeIndex destination) {
142 const auto outgoing_arcs_iter = graph.OutgoingArcs(source);
143 const auto arc =
144 std::find_if(outgoing_arcs_iter.begin(), outgoing_arcs_iter.end(),
145 [&graph, destination](const ArcIndex arc) {
146 return graph.Head(arc) == destination;
147 });
148 return (arc != outgoing_arcs_iter.end()) ? *arc : GraphType::kNilArc;
149}
150
151// Determines the shortest path from the given source and destination, returns a
152// tuple with the path (as a vector of node indices) and its cost.
153template <class GraphType>
154std::tuple<std::vector<NodeIndex>, PathDistance> ComputeShortestPath(
155 const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
156 const NodeIndex source, const NodeIndex destination) {
158 &arc_lengths);
159 dijkstra.RunBoundedDijkstra(source, kMaxDistance);
160 const PathDistance path_length = dijkstra.distances()[destination];
161
162 if (path_length >= kMaxDistance) {
163 // There are shortest paths in this graph, just not from the source to this
164 // destination.
165 // This case only happens when some arcs have an infinite length (i.e.
166 // larger than `kMaxDistance`): `BoundedDijkstraWrapper::NodePathTo` fails
167 // to return a path, even empty.
168 return {{}, kDisconnectedDistance};
169 }
170
171 if (std::vector<NodeIndex> path = std::move(dijkstra.NodePathTo(destination));
172 !path.empty()) {
173 return {std::move(path), path_length};
174 } else {
175 return {{}, kDisconnectedDistance};
176 }
177}
178
179// Computes the total length of a path.
180template <class GraphType>
181PathDistance ComputePathLength(const GraphType& graph,
182 const absl::Span<const PathDistance> arc_lengths,
183 const absl::Span<const NodeIndex> path) {
185 for (NodeIndex i = 0; i < path.size() - 1; ++i) {
186 const ArcIndex arc = internal::FindArcIndex(graph, path[i], path[i + 1]);
187 DCHECK_NE(arc, GraphType::kNilArc);
189 }
190 return distance;
191}
192
193// Stores a path with a priority (typically, the distance), with a comparison
194// operator that operates on the priority.
195class PathWithPriority {
196 public:
197 PathWithPriority(PathDistance priority, std::vector<NodeIndex> path)
198 : path_(std::move(path)), priority_(priority) {}
199 bool operator<(const PathWithPriority& other) const {
200 return priority_ < other.priority_;
201 }
202
203 [[nodiscard]] const std::vector<NodeIndex>& path() const { return path_; }
204 [[nodiscard]] PathDistance priority() const { return priority_; }
206 private:
207 std::vector<NodeIndex> path_;
208 PathDistance priority_;
209};
210
211// Container adapter to be used with STL container adapters such as
212// std::priority_queue. It gives access to the underlying container, which is a
213// protected member in a standard STL container adapter.
214template <class Container>
215class UnderlyingContainerAdapter : public Container {
216 public:
217 typedef typename Container::container_type container_type;
218 // No mutable version of `container`, so that the user cannot change the data
219 // within the container: they might destroy the container's invariants.
220 [[nodiscard]] const container_type& container() const { return this->c; }
222
223} // namespace internal
224
225// TODO(user): Yen's algorithm can work with negative weights, but
226// Dijkstra cannot.
227//
228// Yen, Jin Y. "Finding the k Shortest Loopless Paths in a Network". Management
229// Science. 17 (11): 712–716, 1971.
230// https://doi.org/10.1287%2Fmnsc.17.11.712
231//
232// Yen's notations:
233// - Source node: (1).
234// - Destination node: (N).
235// - Path from (1) to (j): (1) - (i) - ... - (j).
236// - Cost for following the arc from (i) to (j), potentially negative: d_ij.
237// - k-th shortest path: A^k == (1) - (2^k) - (3^k) - ... - (Q_k^k) - (N).
238// - Deviation from A^k-1 at (i): A_i^k. This is the shortest path from (1) to
239// (N) that is identical to A^k-1 from (1) to (i^k-1), then different from all
240// the first k-1 shortest paths {A^1, A^2, ..., A^k-1}.
241// - Root of A_i^k: R_i^k. This is the first subpath of A_i^k that coincides
242// with A^k-1, i.e. A_i^k until i^k-1.
243// - Spur of A_i^k: S_i^k. This is the last subpart of A_i^k with only one node
244// coinciding with A_i^k, (i^k-1), i.e. A_i^k from i^k-1 onwards.
245//
246// Example graph, paths from A to H (more classical notations):
247// C - D
248// / / \
249// A - B / G - H
250// \ / /
251// E - F
252// Source node: A. Destination node: H.
253// Three paths from A to H, say they are ordered from the cheapest to the most
254// expensive:
255// - 1st path: A - B - C - D - G - H
256// - 2nd path: A - B - E - F - G - H
257// - 3rd path: A - B - E - D - G - H
258// To start with, Yen's algorithm uses the shortest path:
259// A^1 = A - B - C - D - G - H
260// To compute the second path A^2, compute a detour around A^1. Consider the
261// iteration where B is the spur node.
262// - Spur node: 2^1 = B.
263// - Root of A^1_2: R_1^2 = A - B (including the spur node 2^1 = B).
264// - Spur path S_1^2 starts at the spur node 2^1 = B. There are two possible
265// spur paths, the cheapest being:
266// S_1^2 = B - E - F - G - H
267template <class GraphType>
268KShortestPaths YenKShortestPaths(const GraphType& graph,
269 const std::vector<PathDistance>& arc_lengths,
270 NodeIndex source, NodeIndex destination,
271 unsigned k) {
273
274 CHECK_GE(k, 0) << "k must be nonnegative. Input value: " << k;
275 CHECK_NE(k, 0) << "k cannot be zero: you are requesting zero paths!";
276
277 CHECK_GT(graph.num_nodes(), 0) << "The graph is empty: it has no nodes";
278 CHECK_GT(graph.num_arcs(), 0) << "The graph is empty: it has no arcs";
279
280 CHECK_GE(source, 0) << "The source node must be nonnegative. Input value: "
281 << source;
282 CHECK_LT(source, graph.num_nodes())
283 << "The source node must be a valid node. Input value: " << source
284 << ". Number of nodes in the input graph: " << graph.num_nodes();
285 CHECK_GE(destination, 0)
286 << "The source node must be nonnegative. Input value: " << destination;
287 CHECK_LT(destination, graph.num_nodes())
288 << "The destination node must be a valid node. Input value: "
289 << destination
290 << ". Number of nodes in the input graph: " << graph.num_nodes();
291
292 KShortestPaths paths;
293
294 // First step: compute the shortest path.
295 {
296 std::tuple<std::vector<NodeIndex>, PathDistance> first_path =
297 internal::ComputeShortestPath(graph, arc_lengths, source, destination);
298 if (std::get<0>(first_path).empty()) return paths;
299 paths.paths.push_back(std::move(std::get<0>(first_path)));
300 paths.distances.push_back(std::get<1>(first_path));
301 }
302
303 if (k == 1) {
304 return paths;
305 }
306
307 // Generate variant paths.
309 std::priority_queue<internal::PathWithPriority>>
310 variant_path_queue;
311
312 // One path has already been generated (the shortest one). Only k-1 more
313 // paths need to be generated.
314 for (; k > 1; --k) {
315 VLOG(1) << "k: " << k;
316
317 // Generate variant paths from the last shortest path.
318 const absl::Span<NodeIndex> last_shortest_path =
319 absl::MakeSpan(paths.paths.back());
320
321 // TODO(user): think about adding parallelism for this loop to improve
322 // running times. This is not a priority as long as the algorithm is
323 // faster than the one in `shortest_paths.h`.
324 for (int spur_node_position = 0;
325 spur_node_position < last_shortest_path.size() - 1;
326 ++spur_node_position) {
327 VLOG(4) << " spur_node_position: " << spur_node_position;
328 VLOG(4) << " last_shortest_path: "
329 << absl::StrJoin(last_shortest_path, " - ") << " ("
330 << last_shortest_path.size() << ")";
331 if (spur_node_position > 0) {
332 DCHECK_NE(last_shortest_path[spur_node_position], source);
333 }
334 DCHECK_NE(last_shortest_path[spur_node_position], destination);
335
336 const NodeIndex spur_node = last_shortest_path[spur_node_position];
337 // Consider the part of the last shortest path up to and excluding the
338 // spur node. If spur_node_position == 0, this span only contains the
339 // source node.
340 const absl::Span<NodeIndex> root_path =
341 last_shortest_path.subspan(0, spur_node_position + 1);
342 DCHECK_GE(root_path.length(), 1);
343 DCHECK_NE(root_path.back(), destination);
344
345 // Simplify the graph to have different paths using infinite lengths:
346 // copy the weights, set some of them to infinity. There is no need to
347 // restore the graph to its previous state in this case.
348 //
349 // This trick is used in the original article (it's old-fashioned), but
350 // not in Wikipedia's pseudocode (it prefers mutating the graph, which is
351 // harder to do without copying the whole graph structure).
352 // Copying the whole graph might be quite expensive, especially as it is
353 // not useful for long (computing one shortest path).
354 std::vector<PathDistance> arc_lengths_for_detour = arc_lengths;
355 for (absl::Span<const NodeIndex> previous_path : paths.paths) {
356 // Check among the previous paths: if part of the path coincides with
357 // the first few nodes up to the spur node (included), forbid this part
358 // of the path in the search for the next shortest path. More
359 // precisely, in that case, avoid the arc from the spur node to the
360 // next node in the path.
361 if (previous_path.size() <= root_path.length()) continue;
362 const bool has_same_prefix_as_root_path = std::equal(
363 root_path.begin(), root_path.end(), previous_path.begin(),
364 previous_path.begin() + root_path.length());
365 if (!has_same_prefix_as_root_path) continue;
366
367 const ArcIndex after_spur_node_arc =
368 internal::FindArcIndex(graph, previous_path[spur_node_position],
369 previous_path[spur_node_position + 1]);
370 VLOG(4) << " after_spur_node_arc: " << graph.Tail(after_spur_node_arc)
371 << " - " << graph.Head(after_spur_node_arc) << " (" << source
372 << " - " << destination << ")";
373 arc_lengths_for_detour[after_spur_node_arc] =
375 }
376 // Ensure that the path computed from the new weights is loopless by
377 // "removing" the nodes of the root path from the graph (by tweaking the
378 // weights, again). The previous operation only disallows the arc from the
379 // spur node (at the end of the root path) to the next node in the
380 // previously found paths.
381 for (int node_position = 0; node_position < spur_node_position;
382 ++node_position) {
383 for (const int arc : graph.OutgoingArcs(root_path[node_position])) {
384 arc_lengths_for_detour[arc] = internal::kDisconnectedDistance;
385 }
386 }
387 VLOG(3) << " arc_lengths_for_detour: "
388 << absl::StrJoin(arc_lengths_for_detour, " - ");
389
390 // Generate a new candidate path from the spur node to the destination
391 // without using the forbidden arcs.
392 {
393 std::tuple<std::vector<NodeIndex>, PathDistance> detour_path =
394 internal::ComputeShortestPath(graph, arc_lengths_for_detour,
395 spur_node, destination);
396
397 if (std::get<0>(detour_path).empty()) {
398 // Node unreachable after some arcs are forbidden.
399 continue;
400 }
401 VLOG(2) << " detour_path: "
402 << absl::StrJoin(std::get<0>(detour_path), " - ") << " ("
403 << std::get<0>(detour_path).size()
404 << "): " << std::get<1>(detour_path);
405 std::vector<NodeIndex> spur_path = std::move(std::get<0>(detour_path));
406 if (ABSL_PREDICT_FALSE(spur_path.empty())) continue;
407
408#ifndef NDEBUG
409 CHECK_EQ(root_path.back(), spur_path.front());
410 CHECK_EQ(spur_node, spur_path.front());
411
412 if (spur_path.size() == 1) {
413 CHECK_EQ(spur_path.front(), destination);
414 } else {
415 // Ensure there is an edge between the end of the root path
416 // and the beginning of the spur path (knowing that both subpaths
417 // coincide at the spur node).
418 const bool root_path_leads_to_spur_path = absl::c_any_of(
419 graph.OutgoingArcs(root_path.back()),
420 [&graph, node_after_spur_in_spur_path =
421 *(spur_path.begin() + 1)](const ArcIndex arc_index) {
422 return graph.Head(arc_index) == node_after_spur_in_spur_path;
423 });
424 CHECK(root_path_leads_to_spur_path);
425 }
426
427 // Ensure the forbidden arc is not present in any previously generated
428 // path.
429 for (absl::Span<const NodeIndex> previous_path : paths.paths) {
430 if (previous_path.size() <= spur_node_position + 1) continue;
431 const bool has_same_prefix_as_root_path = std::equal(
432 root_path.begin(), root_path.end(), previous_path.begin(),
433 previous_path.begin() + root_path.length());
434 if (has_same_prefix_as_root_path) {
435 CHECK_NE(spur_path[1], previous_path[spur_node_position + 1])
436 << "Forbidden arc " << previous_path[spur_node_position]
437 << " - " << previous_path[spur_node_position + 1]
438 << " is present in the spur path "
439 << absl::StrJoin(spur_path, " - ");
440 }
441 }
442#endif // !defined(NDEBUG)
443
444 // Assemble the new path.
445 std::vector<NodeIndex> new_path;
446 absl::c_copy(root_path.subspan(0, spur_node_position),
447 std::back_inserter(new_path));
448 absl::c_copy(spur_path, std::back_inserter(new_path));
449
450 DCHECK_EQ(new_path.front(), source);
451 DCHECK_EQ(new_path.back(), destination);
452
453#ifndef NDEBUG
454 // Ensure the assembled path is loopless, i.e. no node is repeated.
455 {
456 absl::flat_hash_set<NodeIndex> visited_nodes(new_path.begin(),
457 new_path.end());
458 CHECK_EQ(visited_nodes.size(), new_path.size());
459 }
460#endif // !defined(NDEBUG)
461
462 // Ensure the new path is not one of the previously known ones. This
463 // operation is required, as there are two sources of paths from the
464 // source to the destination:
465 // - `paths`, the list of paths that is output by the function: there
466 // is no possible duplicate due to `arc_lengths_for_detour`, where
467 // edges that might generate a duplicate path are forbidden.
468 // - `variant_path_queue`, the list of potential paths, ordered by
469 // their cost, with no impact on `arc_lengths_for_detour`.
470 // TODO(user): would it be faster to fingerprint the paths and
471 // filter by fingerprints? Due to the probability of error with
472 // fingerprints, still use this slow-but-exact code, but after
473 // filtering.
474 const bool is_new_path_already_known =
475 std::any_of(variant_path_queue.container().cbegin(),
476 variant_path_queue.container().cend(),
477 [&new_path](const internal::PathWithPriority& element) {
478 return element.path() == new_path;
479 });
480 if (is_new_path_already_known) continue;
481
482 const PathDistance path_length =
484 VLOG(5) << " New potential path generated: "
485 << absl::StrJoin(new_path, " - ") << " (" << new_path.size()
486 << ")";
487 VLOG(5) << " Root: " << absl::StrJoin(root_path, " - ") << " ("
488 << root_path.size() << ")";
489 VLOG(5) << " Spur: " << absl::StrJoin(spur_path, " - ") << " ("
490 << spur_path.size() << ")";
491 variant_path_queue.emplace(
492 /*priority=*/path_length, /*path=*/std::move(new_path));
493 }
494 }
495
496 // Add the shortest spur path ever found that has not yet been added. This
497 // can be a spur path that has just been generated or a previous one, if
498 // this iteration found no shorter one.
499 if (variant_path_queue.empty()) break;
500
501 const internal::PathWithPriority& next_shortest_path =
502 variant_path_queue.top();
503 VLOG(5) << "> New path generated: "
504 << absl::StrJoin(next_shortest_path.path(), " - ") << " ("
505 << next_shortest_path.path().size() << ")";
506 paths.paths.emplace_back(next_shortest_path.path());
507 paths.distances.push_back(next_shortest_path.priority());
508 variant_path_queue.pop();
509 }
510
511 return paths;
512}
513
514} // namespace operations_research
515
516#endif // OR_TOOLS_GRAPH_K_SHORTEST_PATHS_H_
std::vector< int > NodePathTo(int node) const
const std::vector< DistanceType > & distances() const
The distance of the nodes from their source.
const std::vector< int > & RunBoundedDijkstra(int source_node, DistanceType distance_limit)
PathWithPriority(PathDistance priority, std::vector< NodeIndex > path)
bool operator<(const PathWithPriority &other) const
const std::vector< NodeIndex > & path() const
std::vector< double > arc_lengths
GraphType graph
int arc
PathDistance ComputePathLength(const GraphType &graph, const absl::Span< const PathDistance > arc_lengths, const absl::Span< const NodeIndex > path)
Computes the total length of a path.
std::tuple< std::vector< NodeIndex >, PathDistance > ComputeShortestPath(const GraphType &graph, const std::vector< PathDistance > &arc_lengths, const NodeIndex source, const NodeIndex destination)
const PathDistance kDisconnectedDistance
ArcIndex FindArcIndex(const GraphType &graph, const NodeIndex source, const NodeIndex destination)
In SWIG mode, we don't want anything besides these top-level includes.
KShortestPaths YenKShortestPaths(const GraphType &graph, const std::vector< PathDistance > &arc_lengths, NodeIndex source, NodeIndex destination, unsigned k)
STL namespace.
double distance
std::vector< PathDistance > distances
std::vector< std::vector< NodeIndex > > paths