Google OR-Tools v9.12
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-2025 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"
74
75namespace operations_research {
76
77// Stores the solution to a k-shortest path problem. `paths` contains up to `k`
78// paths from `source` to `destination` (these nodes are arguments to the
79// algorithm), each having a distance stored in `distances`.
80//
81// The paths in `paths` start with `origin` and end at `destination`.
82//
83// If the computations are unsuccessful for any reason, the vectors are empty.
84template <class GraphType>
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<typename GraphType::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>
117 const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
118 typename GraphType::NodeIndex source,
119 typename GraphType::NodeIndex destination, 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>
140typename GraphType::ArcIndex FindArcIndex(
141 const GraphType& graph, const typename GraphType::NodeIndex source,
142 const typename GraphType::NodeIndex destination) {
143 const auto outgoing_arcs_iter = graph.OutgoingArcs(source);
144 const auto arc = std::find_if(
145 outgoing_arcs_iter.begin(), outgoing_arcs_iter.end(),
146 [&graph, destination](const typename GraphType::ArcIndex arc) {
147 return graph.Head(arc) == destination;
148 });
149 return (arc != outgoing_arcs_iter.end()) ? *arc : GraphType::kNilArc;
150}
151
152// Determines the shortest path from the given source and destination, returns a
153// tuple with the path (as a vector of node indices) and its cost.
154template <class GraphType>
155std::tuple<std::vector<typename GraphType::NodeIndex>, PathDistance>
156ComputeShortestPath(const GraphType& graph,
157 const std::vector<PathDistance>& arc_lengths,
158 const typename GraphType::NodeIndex source,
159 const typename GraphType::NodeIndex destination) {
161 &arc_lengths);
162 dijkstra.RunBoundedDijkstra(source, kMaxDistance);
163 const PathDistance path_length = dijkstra.distances()[destination];
164
165 if (path_length >= kMaxDistance) {
166 // There are shortest paths in this graph, just not from the source to this
167 // destination.
168 // This case only happens when some arcs have an infinite length (i.e.
169 // larger than `kMaxDistance`): `BoundedDijkstraWrapper::NodePathTo` fails
170 // to return a path, even empty.
171 return {std::vector<typename GraphType::NodeIndex>{},
173 }
174
175 if (std::vector<typename GraphType::NodeIndex> path =
176 std::move(dijkstra.NodePathTo(destination));
177 !path.empty()) {
178 return {std::move(path), path_length};
179 } else {
180 return {std::vector<typename GraphType::NodeIndex>{},
182 }
183}
184
185// Computes the total length of a path.
186template <class GraphType>
188 const GraphType& graph, const absl::Span<const PathDistance> arc_lengths,
189 const absl::Span<const typename GraphType::NodeIndex> path) {
190 PathDistance distance = 0;
191 for (typename GraphType::NodeIndex i = 0; i < path.size() - 1; ++i) {
192 const typename GraphType::ArcIndex arc =
193 internal::FindArcIndex(graph, path[i], path[i + 1]);
194 DCHECK_NE(arc, GraphType::kNilArc);
195 distance += arc_lengths[arc];
196 }
197 return distance;
198}
199
200// Stores a path with a priority (typically, the distance), with a comparison
201// operator that operates on the priority.
202template <class GraphType>
203class PathWithPriority {
204 public:
205 using NodeIndex = typename GraphType::NodeIndex;
207 PathWithPriority(PathDistance priority, std::vector<NodeIndex> path)
208 : path_(std::move(path)), priority_(priority) {}
209 bool operator<(const PathWithPriority& other) const {
210 return priority_ < other.priority_;
211 }
212
213 [[nodiscard]] const std::vector<NodeIndex>& path() const { return path_; }
214 [[nodiscard]] PathDistance priority() const { return priority_; }
216 private:
217 std::vector<NodeIndex> path_;
218 PathDistance priority_;
219};
220
221// Container adapter to be used with STL container adapters such as
222// std::priority_queue. It gives access to the underlying container, which is a
223// protected member in a standard STL container adapter.
224template <class Container>
225class UnderlyingContainerAdapter : public Container {
226 public:
227 typedef typename Container::container_type container_type;
228 // No mutable version of `container`, so that the user cannot change the data
229 // within the container: they might destroy the container's invariants.
230 [[nodiscard]] const container_type& container() const { return this->c; }
232
233} // namespace internal
234
235// TODO(user): Yen's algorithm can work with negative weights, but
236// Dijkstra cannot.
237//
238// Yen, Jin Y. "Finding the k Shortest Loopless Paths in a Network". Management
239// Science. 17 (11): 712–716, 1971.
240// https://doi.org/10.1287%2Fmnsc.17.11.712
241//
242// Yen's notations:
243// - Source node: (1).
244// - Destination node: (N).
245// - Path from (1) to (j): (1) - (i) - ... - (j).
246// - Cost for following the arc from (i) to (j), potentially negative: d_ij.
247// - k-th shortest path: A^k == (1) - (2^k) - (3^k) - ... - (Q_k^k) - (N).
248// - Deviation from A^k-1 at (i): A_i^k. This is the shortest path from (1) to
249// (N) that is identical to A^k-1 from (1) to (i^k-1), then different from all
250// the first k-1 shortest paths {A^1, A^2, ..., A^k-1}.
251// - Root of A_i^k: R_i^k. This is the first subpath of A_i^k that coincides
252// with A^k-1, i.e. A_i^k until i^k-1.
253// - Spur of A_i^k: S_i^k. This is the last subpart of A_i^k with only one node
254// coinciding with A_i^k, (i^k-1), i.e. A_i^k from i^k-1 onwards.
255//
256// Example graph, paths from A to H (more classical notations):
257// C - D
258// / / \
259// A - B / G - H
260// \ / /
261// E - F
262// Source node: A. Destination node: H.
263// Three paths from A to H, say they are ordered from the cheapest to the most
264// expensive:
265// - 1st path: A - B - C - D - G - H
266// - 2nd path: A - B - E - F - G - H
267// - 3rd path: A - B - E - D - G - H
268// To start with, Yen's algorithm uses the shortest path:
269// A^1 = A - B - C - D - G - H
270// To compute the second path A^2, compute a detour around A^1. Consider the
271// iteration where B is the spur node.
272// - Spur node: 2^1 = B.
273// - Root of A^1_2: R_1^2 = A - B (including the spur node 2^1 = B).
274// - Spur path S_1^2 starts at the spur node 2^1 = B. There are two possible
275// spur paths, the cheapest being:
276// S_1^2 = B - E - F - G - H
277template <class GraphType>
279 const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
280 typename GraphType::NodeIndex source,
281 typename GraphType::NodeIndex destination, unsigned k) {
282 using NodeIndex = typename GraphType::NodeIndex;
283
285
286 CHECK_GE(k, 0) << "k must be nonnegative. Input value: " << k;
287 CHECK_NE(k, 0) << "k cannot be zero: you are requesting zero paths!";
288
289 CHECK_GT(graph.num_nodes(), 0) << "The graph is empty: it has no nodes";
290 CHECK_GT(graph.num_arcs(), 0) << "The graph is empty: it has no arcs";
291
292 CHECK_GE(source, 0) << "The source node must be nonnegative. Input value: "
293 << source;
294 CHECK_LT(source, graph.num_nodes())
295 << "The source node must be a valid node. Input value: " << source
296 << ". Number of nodes in the input graph: " << graph.num_nodes();
297 CHECK_GE(destination, 0)
298 << "The source node must be nonnegative. Input value: " << destination;
299 CHECK_LT(destination, graph.num_nodes())
300 << "The destination node must be a valid node. Input value: "
301 << destination
302 << ". Number of nodes in the input graph: " << graph.num_nodes();
303
305
306 // First step: compute the shortest path.
307 {
308 std::tuple<std::vector<NodeIndex>, PathDistance> first_path =
309 internal::ComputeShortestPath(graph, arc_lengths, source, destination);
310 if (std::get<0>(first_path).empty()) return paths;
311 paths.paths.push_back(std::move(std::get<0>(first_path)));
312 paths.distances.push_back(std::get<1>(first_path));
313 }
314
315 if (k == 1) {
316 return paths;
317 }
318
319 // Generate variant paths.
321 std::priority_queue<internal::PathWithPriority<GraphType>>>
322 variant_path_queue;
323
324 // One path has already been generated (the shortest one). Only k-1 more
325 // paths need to be generated.
326 for (; k > 1; --k) {
327 VLOG(1) << "k: " << k;
328
329 // Generate variant paths from the last shortest path.
330 const absl::Span<NodeIndex> last_shortest_path =
331 absl::MakeSpan(paths.paths.back());
332
333 // TODO(user): think about adding parallelism for this loop to improve
334 // running times. This is not a priority as long as the algorithm is
335 // faster than the one in `shortest_paths.h`.
336 for (int spur_node_position = 0;
337 spur_node_position < last_shortest_path.size() - 1;
338 ++spur_node_position) {
339 VLOG(4) << " spur_node_position: " << spur_node_position;
340 VLOG(4) << " last_shortest_path: "
341 << absl::StrJoin(last_shortest_path, " - ") << " ("
342 << last_shortest_path.size() << ")";
343 if (spur_node_position > 0) {
344 DCHECK_NE(last_shortest_path[spur_node_position], source);
345 }
346 DCHECK_NE(last_shortest_path[spur_node_position], destination);
347
348 const NodeIndex spur_node = last_shortest_path[spur_node_position];
349 // Consider the part of the last shortest path up to and excluding the
350 // spur node. If spur_node_position == 0, this span only contains the
351 // source node.
352 const absl::Span<NodeIndex> root_path =
353 last_shortest_path.subspan(0, spur_node_position + 1);
354 DCHECK_GE(root_path.length(), 1);
355 DCHECK_NE(root_path.back(), destination);
356
357 // Simplify the graph to have different paths using infinite lengths:
358 // copy the weights, set some of them to infinity. There is no need to
359 // restore the graph to its previous state in this case.
360 //
361 // This trick is used in the original article (it's old-fashioned), but
362 // not in Wikipedia's pseudocode (it prefers mutating the graph, which is
363 // harder to do without copying the whole graph structure).
364 // Copying the whole graph might be quite expensive, especially as it is
365 // not useful for long (computing one shortest path).
366 std::vector<PathDistance> arc_lengths_for_detour = arc_lengths;
367 for (absl::Span<const NodeIndex> previous_path : paths.paths) {
368 // Check among the previous paths: if part of the path coincides with
369 // the first few nodes up to the spur node (included), forbid this part
370 // of the path in the search for the next shortest path. More
371 // precisely, in that case, avoid the arc from the spur node to the
372 // next node in the path.
373 if (previous_path.size() <= root_path.length()) continue;
374 const bool has_same_prefix_as_root_path = std::equal(
375 root_path.begin(), root_path.end(), previous_path.begin(),
376 previous_path.begin() + root_path.length());
377 if (!has_same_prefix_as_root_path) continue;
378
379 const typename GraphType::ArcIndex after_spur_node_arc =
380 internal::FindArcIndex(graph, previous_path[spur_node_position],
381 previous_path[spur_node_position + 1]);
382 VLOG(4) << " after_spur_node_arc: " << graph.Tail(after_spur_node_arc)
383 << " - " << graph.Head(after_spur_node_arc) << " (" << source
384 << " - " << destination << ")";
385 arc_lengths_for_detour[after_spur_node_arc] =
387 }
388 // Ensure that the path computed from the new weights is loopless by
389 // "removing" the nodes of the root path from the graph (by tweaking the
390 // weights, again). The previous operation only disallows the arc from the
391 // spur node (at the end of the root path) to the next node in the
392 // previously found paths.
393 for (int node_position = 0; node_position < spur_node_position;
394 ++node_position) {
395 for (const int arc : graph.OutgoingArcs(root_path[node_position])) {
396 arc_lengths_for_detour[arc] = internal::kDisconnectedDistance;
397 }
398 }
399 VLOG(3) << " arc_lengths_for_detour: "
400 << absl::StrJoin(arc_lengths_for_detour, " - ");
401
402 // Generate a new candidate path from the spur node to the destination
403 // without using the forbidden arcs.
404 {
405 std::tuple<std::vector<NodeIndex>, PathDistance> detour_path =
406 internal::ComputeShortestPath(graph, arc_lengths_for_detour,
407 spur_node, destination);
408
409 if (std::get<0>(detour_path).empty()) {
410 // Node unreachable after some arcs are forbidden.
411 continue;
412 }
413 VLOG(2) << " detour_path: "
414 << absl::StrJoin(std::get<0>(detour_path), " - ") << " ("
415 << std::get<0>(detour_path).size()
416 << "): " << std::get<1>(detour_path);
417 std::vector<NodeIndex> spur_path = std::move(std::get<0>(detour_path));
418 if (ABSL_PREDICT_FALSE(spur_path.empty())) continue;
419
420#ifndef NDEBUG
421 CHECK_EQ(root_path.back(), spur_path.front());
422 CHECK_EQ(spur_node, spur_path.front());
423
424 if (spur_path.size() == 1) {
425 CHECK_EQ(spur_path.front(), destination);
426 } else {
427 // Ensure there is an edge between the end of the root path
428 // and the beginning of the spur path (knowing that both subpaths
429 // coincide at the spur node).
430 const bool root_path_leads_to_spur_path = absl::c_any_of(
431 graph.OutgoingArcs(root_path.back()),
432 [&graph, node_after_spur_in_spur_path = *(spur_path.begin() + 1)](
433 const typename GraphType::ArcIndex arc_index) {
434 return graph.Head(arc_index) == node_after_spur_in_spur_path;
435 });
436 CHECK(root_path_leads_to_spur_path);
437 }
438
439 // Ensure the forbidden arc is not present in any previously generated
440 // path.
441 for (absl::Span<const NodeIndex> previous_path : paths.paths) {
442 if (previous_path.size() <= spur_node_position + 1) continue;
443 const bool has_same_prefix_as_root_path = std::equal(
444 root_path.begin(), root_path.end(), previous_path.begin(),
445 previous_path.begin() + root_path.length());
446 if (has_same_prefix_as_root_path) {
447 CHECK_NE(spur_path[1], previous_path[spur_node_position + 1])
448 << "Forbidden arc " << previous_path[spur_node_position]
449 << " - " << previous_path[spur_node_position + 1]
450 << " is present in the spur path "
451 << absl::StrJoin(spur_path, " - ");
452 }
453 }
454#endif // !defined(NDEBUG)
455
456 // Assemble the new path.
457 std::vector<NodeIndex> new_path;
458 absl::c_copy(root_path.subspan(0, spur_node_position),
459 std::back_inserter(new_path));
460 absl::c_copy(spur_path, std::back_inserter(new_path));
461
462 DCHECK_EQ(new_path.front(), source);
463 DCHECK_EQ(new_path.back(), destination);
464
465#ifndef NDEBUG
466 // Ensure the assembled path is loopless, i.e. no node is repeated.
467 {
468 absl::flat_hash_set<NodeIndex> visited_nodes(new_path.begin(),
469 new_path.end());
470 CHECK_EQ(visited_nodes.size(), new_path.size());
471 }
472#endif // !defined(NDEBUG)
473
474 // Ensure the new path is not one of the previously known ones. This
475 // operation is required, as there are two sources of paths from the
476 // source to the destination:
477 // - `paths`, the list of paths that is output by the function: there
478 // is no possible duplicate due to `arc_lengths_for_detour`, where
479 // edges that might generate a duplicate path are forbidden.
480 // - `variant_path_queue`, the list of potential paths, ordered by
481 // their cost, with no impact on `arc_lengths_for_detour`.
482 // TODO(user): would it be faster to fingerprint the paths and
483 // filter by fingerprints? Due to the probability of error with
484 // fingerprints, still use this slow-but-exact code, but after
485 // filtering.
486 const bool is_new_path_already_known = std::any_of(
487 variant_path_queue.container().cbegin(),
488 variant_path_queue.container().cend(),
489 [&new_path](const internal::PathWithPriority<GraphType>& element) {
490 return element.path() == new_path;
491 });
492 if (is_new_path_already_known) continue;
493
494 const PathDistance path_length =
495 internal::ComputePathLength(graph, arc_lengths, new_path);
496 VLOG(5) << " New potential path generated: "
497 << absl::StrJoin(new_path, " - ") << " (" << new_path.size()
498 << ")";
499 VLOG(5) << " Root: " << absl::StrJoin(root_path, " - ") << " ("
500 << root_path.size() << ")";
501 VLOG(5) << " Spur: " << absl::StrJoin(spur_path, " - ") << " ("
502 << spur_path.size() << ")";
503 variant_path_queue.emplace(
504 /*priority=*/path_length, /*path=*/std::move(new_path));
505 }
506 }
507
508 // Add the shortest spur path ever found that has not yet been added. This
509 // can be a spur path that has just been generated or a previous one, if
510 // this iteration found no shorter one.
511 if (variant_path_queue.empty()) break;
512
513 const internal::PathWithPriority<GraphType>& next_shortest_path =
514 variant_path_queue.top();
515 VLOG(5) << "> New path generated: "
516 << absl::StrJoin(next_shortest_path.path(), " - ") << " ("
517 << next_shortest_path.path().size() << ")";
518 paths.paths.emplace_back(next_shortest_path.path());
519 paths.distances.push_back(next_shortest_path.priority());
520 variant_path_queue.pop();
521 }
522
523 return paths;
524}
525
526} // namespace operations_research
527
528#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)
const std::vector< NodeIndex > & path() const
bool operator<(const PathWithPriority &other) const
PathWithPriority(PathDistance priority, std::vector< NodeIndex > path)
const PathDistance kDisconnectedDistance
std::tuple< std::vector< typename GraphType::NodeIndex >, PathDistance > ComputeShortestPath(const GraphType &graph, const std::vector< PathDistance > &arc_lengths, const typename GraphType::NodeIndex source, const typename GraphType::NodeIndex destination)
GraphType::ArcIndex FindArcIndex(const GraphType &graph, const typename GraphType::NodeIndex source, const typename GraphType::NodeIndex destination)
PathDistance ComputePathLength(const GraphType &graph, const absl::Span< const PathDistance > arc_lengths, const absl::Span< const typename GraphType::NodeIndex > path)
Computes the total length of a path.
In SWIG mode, we don't want anything besides these top-level includes.
KShortestPaths< GraphType > YenKShortestPaths(const GraphType &graph, const std::vector< PathDistance > &arc_lengths, typename GraphType::NodeIndex source, typename GraphType::NodeIndex destination, unsigned k)
STL namespace.
std::vector< std::vector< typename GraphType::NodeIndex > > paths
std::vector< PathDistance > distances