Files
ortools-clone/ortools/graph/bounded_dijkstra_test.cc
Corentin Le Molgat 35ee27b271 graph: export from google3
dump_vars: Add support for StrongInt and StrongVector
2025-06-16 15:33:46 +02:00

785 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/bounded_dijkstra.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <random>
#include <type_traits>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/random/distributions.h"
#include "absl/random/random.h"
#include "benchmark/benchmark.h"
#include "gtest/gtest.h"
#include "ortools/base/dump_vars.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/graph/graph.h"
#include "ortools/graph/graph_io.h"
#include "ortools/graph/test_util.h"
#include "ortools/util/flat_matrix.h"
namespace operations_research {
namespace {
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::UnorderedElementsAreArray;
using ::util::ListGraph;
DEFINE_STRONG_INT_TYPE(NodeIndex, int32_t);
DEFINE_STRONG_INT_TYPE(ArcIndex, int64_t);
using TestGraph = ListGraph<NodeIndex, ArcIndex>;
template <typename DistanceType>
using DijkstraWrapper = BoundedDijkstraWrapper<TestGraph, DistanceType>;
TEST(BoundedDijkstraWrapperDeathTest, Accessors) {
TestGraph graph;
graph.AddArc(NodeIndex(1), NodeIndex(3));
DijkstraWrapper<float>::ByArc<float> arc_lengths = {2.5};
DijkstraWrapper<float> dijkstra(&graph, &arc_lengths);
const std::is_same<float, decltype(dijkstra)::distance_type> same_type;
ASSERT_TRUE(same_type.value);
ASSERT_EQ(&dijkstra.graph(), &graph);
ASSERT_EQ(dijkstra.GetArcLength(ArcIndex(0)), 2.5);
}
TEST(BoundedDijkstraWrapperDeathTest, WithArcLengthFunctor) {
TestGraph graph;
graph.AddArc(NodeIndex(1), NodeIndex(3));
BoundedDijkstraWrapper<TestGraph, float, std::function<float(ArcIndex)>>
dijkstra(&graph, [](ArcIndex) { return 2.34; });
ASSERT_FLOAT_EQ(dijkstra.GetArcLength(ArcIndex(0)), 2.34f);
}
TEST(BoundedDijkstraWrapperDeathTest, ConstructorPreconditions) {
TestGraph graph;
for (int i = 0; i < 50; ++i) graph.AddArc(NodeIndex(i), NodeIndex(i + 1));
typedef DijkstraWrapper<int> TestedClass;
TestedClass::ByArc<int> arc_lengths(13, 0);
EXPECT_DEATH(new TestedClass(&graph, &arc_lengths), "13");
arc_lengths.resize(50, 0);
arc_lengths[ArcIndex(20)] = -132;
EXPECT_DEATH(new TestedClass(&graph, &arc_lengths), "-132");
}
TEST(BoundedDijkstraWrapper, ArcPathToAndSourceOfShortestPathToNode) {
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths = {1, 2, 3, 4, 6, 5};
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(1), NodeIndex(2));
graph.AddArc(NodeIndex(1), NodeIndex(2));
graph.AddArc(NodeIndex(2), NodeIndex(3));
graph.AddArc(NodeIndex(2), NodeIndex(3));
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 10);
EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2),
NodeIndex(3)));
EXPECT_EQ(9, dijkstra.distances()[NodeIndex(3)]);
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)),
ElementsAre(ArcIndex(0), ArcIndex(2), ArcIndex(5)));
EXPECT_THAT(
dijkstra.NodePathTo(NodeIndex(3)),
ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), NodeIndex(3)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3)));
}
TEST(BoundedDijkstraWrapper, EmptyPath) {
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths = {1, 2};
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(2), NodeIndex(3));
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 10);
EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1)));
EXPECT_EQ(0, dijkstra.distances()[NodeIndex(0)]);
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre());
EXPECT_THAT(dijkstra.NodePathTo(NodeIndex(0)), ElementsAre(NodeIndex(0)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(0)));
}
TEST(BoundedDijkstraWrapper, OverflowSafe) {
TestGraph graph;
const int64_t int_max = std::numeric_limits<int64_t>::max();
DijkstraWrapper<int64_t>::ByArc<int64_t> arc_lengths = {int_max, int_max / 2,
int_max / 2, 1};
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(1), NodeIndex(2));
graph.AddArc(NodeIndex(2), NodeIndex(3));
BoundedDijkstraWrapper<TestGraph, int64_t> dijkstra(&graph, &arc_lengths);
const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), int_max);
// This works because int_max is odd, i.e. 2 * (int_max / 2) = int_max - 1
EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2)));
EXPECT_EQ(0, dijkstra.distances()[NodeIndex(0)]);
EXPECT_EQ(int_max / 2, dijkstra.distances()[NodeIndex(1)]);
EXPECT_EQ(int_max - 1, dijkstra.distances()[NodeIndex(2)]);
}
TEST(BoundedDijkstraWrapper,
ArcPathToAndSourceOfShortestPathToNode_WithArcLengthFunction) {
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths = {1, 2, 3, 4, 6, 5};
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(1), NodeIndex(2));
graph.AddArc(NodeIndex(1), NodeIndex(2));
graph.AddArc(NodeIndex(2), NodeIndex(3));
graph.AddArc(NodeIndex(2), NodeIndex(3));
class MyArcLengthFunctor {
public:
explicit MyArcLengthFunctor(
const DijkstraWrapper<int>::ByArc<int>& arc_lengths)
: arc_lengths_(arc_lengths) {}
int operator()(ArcIndex arc) const {
return arc.value() % 2 == 1 ? arc_lengths_[arc] : 100;
}
private:
const DijkstraWrapper<int>::ByArc<int>& arc_lengths_;
};
BoundedDijkstraWrapper<TestGraph, int, MyArcLengthFunctor> dijkstra(
&graph, MyArcLengthFunctor(arc_lengths));
const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 20);
EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2),
NodeIndex(3)));
EXPECT_EQ(11, dijkstra.distances()[NodeIndex(3)]);
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)),
ElementsAre(ArcIndex(1), ArcIndex(3), ArcIndex(5)));
EXPECT_THAT(
dijkstra.NodePathTo(NodeIndex(3)),
ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), NodeIndex(3)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3)));
}
TEST(BoundedDijkstraWrapperTest, RandomDenseGraph) {
std::mt19937 random(12345);
const int num_nodes = 50;
std::vector<std::vector<int>> lengths(num_nodes, std::vector<int>(num_nodes));
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
for (int i = 0; i < num_nodes; ++i) {
for (int j = 0; j < num_nodes; ++j) {
lengths[i][j] = (i == j) ? 0 : absl::Uniform(random, 0, 1000);
graph.AddArc(NodeIndex(i), NodeIndex(j));
arc_lengths.push_back(lengths[i][j]);
}
}
// Compute the shortest-path length using FloydWarshall algorithm.
for (int k = 0; k < num_nodes; ++k) {
for (int i = 0; i < num_nodes; ++i) {
for (int j = 0; j < num_nodes; ++j) {
lengths[i][j] = std::min(lengths[i][j], lengths[i][k] + lengths[k][j]);
}
}
}
// Test the bounded dijkstra code (from all sources).
std::vector<int> reached_sizes;
for (int source = 0; source < num_nodes; ++source) {
const int limit = 100;
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(source), limit);
for (const NodeIndex node : reached) {
EXPECT_LT(dijkstra.distances()[node], limit);
EXPECT_EQ(dijkstra.distances()[node], lengths[source][node.value()]);
// Check that we never have the same node twice in the paths.
std::vector<NodeIndex> path = {node};
NodeIndex parent = node;
while (dijkstra.parents()[parent] != parent) {
parent = dijkstra.parents()[parent];
path.push_back(parent);
}
std::sort(path.begin(), path.end());
EXPECT_THAT(std::unique(path.begin(), path.end()), path.end());
}
int num_under_limit = 0;
for (int i = 0; i < num_nodes; ++i) {
if (lengths[source][i] < limit) ++num_under_limit;
}
EXPECT_EQ(num_under_limit, reached.size());
reached_sizes.push_back(reached.size());
}
// With the given limit, we reach a good number of nodes.
EXPECT_THAT(reached_sizes, Contains(15));
EXPECT_THAT(reached_sizes, Contains(28));
EXPECT_THAT(reached_sizes, Contains(41));
}
TEST(SimpleOneToOneShortestPathTest, PathTooLong) {
const int big_length = std::numeric_limits<int>::max() / 2;
std::vector<int> tails = {0, 1, 2};
std::vector<int> heads = {1, 2, 3};
std::vector<int> lengths = {big_length, big_length, big_length};
{
const auto [distance, path] =
SimpleOneToOneShortestPath<int, int>(0, 3, tails, heads, lengths);
EXPECT_EQ(distance, std::numeric_limits<int>::max());
EXPECT_TRUE(path.empty());
}
{
// from 0 to 2 work because 2 * big_length < int_max.
const auto [distance, path] =
SimpleOneToOneShortestPath<int, int>(0, 2, tails, heads, lengths);
EXPECT_EQ(distance, std::numeric_limits<int>::max() - 1);
EXPECT_THAT(path, ElementsAre(0, 1, 2));
}
}
TEST(SimpleOneToOneShortestPathTest, Random) {
absl::BitGen random;
// We will generate a random dense graph.
const int num_nodes = 50;
std::vector<std::vector<int>> lengths(num_nodes, std::vector<int>(num_nodes));
std::vector<std::vector<int>> shortest_distance(num_nodes,
std::vector<int>(num_nodes));
// This will be the "sparse" representation.
std::vector<int> tails;
std::vector<int> heads;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
// We permutes the arc order to properly test that it do not matter.
std::vector<int> nodes(num_nodes);
for (int i = 0; i < num_nodes; ++i) nodes[i] = i;
std::shuffle(nodes.begin(), nodes.end(), random);
// Generate random data.
for (const int tail : nodes) {
for (const int head : nodes) {
lengths[tail][head] = (tail == head) ? 0 : absl::Uniform(random, 0, 1000);
shortest_distance[tail][head] = lengths[tail][head];
tails.push_back(tail);
heads.push_back(head);
arc_lengths.push_back(lengths[tail][head]);
}
}
// Compute the shortest-path length using FloydWarshall algorithm.
for (int k = 0; k < num_nodes; ++k) {
for (int i = 0; i < num_nodes; ++i) {
for (int j = 0; j < num_nodes; ++j) {
shortest_distance[i][j] =
std::min(shortest_distance[i][j],
shortest_distance[i][k] + shortest_distance[k][j]);
}
}
}
// Test the code from a bunch of random (from, to) pair.
for (int runs = 0; runs < 100; ++runs) {
const int from = absl::Uniform(random, 0, num_nodes);
const int to = absl::Uniform(random, 0, num_nodes);
// No limit. There should always be a path with our generated data.
{
const auto [distance, path] = SimpleOneToOneShortestPath<int, int>(
from, to, tails, heads, arc_lengths);
EXPECT_EQ(distance, shortest_distance[from][to]);
EXPECT_FALSE(path.empty());
EXPECT_EQ(path.front(), from);
EXPECT_EQ(path.back(), to);
}
// A limit of shortest_distance[from][to] + 1 works too.
{
const auto [distance, path] = SimpleOneToOneShortestPath<int, int>(
from, to, tails, heads, arc_lengths, shortest_distance[from][to] + 1);
EXPECT_EQ(distance, shortest_distance[from][to]);
EXPECT_FALSE(path.empty());
EXPECT_EQ(path.front(), from);
EXPECT_EQ(path.back(), to);
}
// But a limit of shortest_distance[from][to] should fail.
{
const auto [distance, path] = SimpleOneToOneShortestPath<int, int>(
from, to, tails, heads, arc_lengths, shortest_distance[from][to]);
EXPECT_EQ(distance, shortest_distance[from][to]);
EXPECT_TRUE(path.empty());
}
}
}
TEST(BoundedDijkstraWrapperTest, MultiRunsOverDynamicGraphAndLengths) {
TestGraph graph;
graph.AddArc(NodeIndex(0), NodeIndex(1));
graph.AddArc(NodeIndex(0), NodeIndex(1));
DijkstraWrapper<int>::ByArc<int> arc_lengths = {4, 3};
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 5),
ElementsAre(NodeIndex(0), NodeIndex(1)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), ElementsAre(ArcIndex(1)));
EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 2),
ElementsAre(NodeIndex(0)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(0)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), IsEmpty());
EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(1), 99),
ElementsAre(NodeIndex(1)));
EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(1)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), IsEmpty());
// Add some arcs and nodes...
graph.AddArc(NodeIndex(0), NodeIndex(2));
arc_lengths.push_back(1);
graph.AddArc(NodeIndex(1), NodeIndex(2));
arc_lengths.push_back(0);
graph.AddArc(NodeIndex(2), NodeIndex(1));
arc_lengths.push_back(1);
graph.AddArc(NodeIndex(1), NodeIndex(3));
arc_lengths.push_back(5);
EXPECT_THAT(
dijkstra.RunBoundedDijkstra(NodeIndex(0), 10),
ElementsAre(NodeIndex(0), NodeIndex(2), NodeIndex(1), NodeIndex(3)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)),
ElementsAre(ArcIndex(2), ArcIndex(4), ArcIndex(5)));
EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 6),
ElementsAre(NodeIndex(0), NodeIndex(2), NodeIndex(1)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)),
ElementsAre(ArcIndex(2), ArcIndex(4)));
}
TEST(BoundedDijkstraWrapperTest, MultipleSources) {
// Use this graph. Source nodes have their initial distance in [ ].
//
// N1[0] --(2)--> N0[4] --(1)--> N2 --(5)--> N3 <--(4)-- N4[3] --(5)--> N5
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
graph.AddArc(NodeIndex(1), NodeIndex(0));
arc_lengths.push_back(2);
graph.AddArc(NodeIndex(0), NodeIndex(2));
arc_lengths.push_back(1);
graph.AddArc(NodeIndex(2), NodeIndex(3));
arc_lengths.push_back(5);
graph.AddArc(NodeIndex(4), NodeIndex(3));
arc_lengths.push_back(4);
graph.AddArc(NodeIndex(4), NodeIndex(5));
arc_lengths.push_back(5);
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
// The distance limit is exclusive, so we can't reach Node 5.
ASSERT_THAT(dijkstra.RunBoundedDijkstraFromMultipleSources(
{{NodeIndex(1), 0}, {NodeIndex(0), 4}, {NodeIndex(4), 3}}, 8),
// The order is deterministic: node 4 comes before node 2, despite
// having equal distance and higher index, because it's a source.
ElementsAre(NodeIndex(1), NodeIndex(0), NodeIndex(4),
NodeIndex(2), NodeIndex(3)));
EXPECT_EQ(2, dijkstra.distances()[NodeIndex(0)]);
EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(0)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre(ArcIndex(0)));
EXPECT_EQ(0, dijkstra.distances()[NodeIndex(1)]);
EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(1)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), IsEmpty());
EXPECT_EQ(3, dijkstra.distances()[NodeIndex(2)]);
EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(2)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)),
ElementsAre(ArcIndex(0), ArcIndex(1)));
EXPECT_EQ(7, dijkstra.distances()[NodeIndex(3)]);
EXPECT_EQ(NodeIndex(4), dijkstra.SourceOfShortestPathToNode(NodeIndex(3)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)), ElementsAre(ArcIndex(3)));
EXPECT_EQ(3, dijkstra.distances()[NodeIndex(4)]);
EXPECT_EQ(NodeIndex(4), dijkstra.SourceOfShortestPathToNode(NodeIndex(4)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(4)), IsEmpty());
}
TEST(BoundedDijkstraWrapperTest, SourcesAtOrBeyondDistanceLimitAreNotReached) {
TestGraph graph(/*num_nodes=*/NodeIndex(5), /*arc_capacity=*/ArcIndex(0));
DijkstraWrapper<int>::ByArc<int> arc_lengths; // No arcs.
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSources({{NodeIndex(0), 10},
{NodeIndex(1), 11},
{NodeIndex(2), 12},
{NodeIndex(3), 13}},
12),
ElementsAre(NodeIndex(0), NodeIndex(1)));
}
TEST(BoundedDijkstraWrapperTest, SourcesListedMultipleTimesKeepsMinDistance) {
TestGraph graph(/*num_nodes=*/NodeIndex(5), /*arc_capacity=*/ArcIndex(1));
graph.AddArc(NodeIndex(1), NodeIndex(3));
DijkstraWrapper<int>::ByArc<int> arc_lengths = {20};
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSources(
{{NodeIndex(1), 12}, {NodeIndex(1), 10}, {NodeIndex(1), 14}}, 31),
ElementsAre(NodeIndex(1), NodeIndex(3)));
EXPECT_EQ(dijkstra.distances()[NodeIndex(3)], 30);
}
TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) {
// Source nodes are "S", destination nodes are "D", and the rest are "N".
// Source and destination nodes have their distance offset in [ ].
//
// S0[2] --(3)--> D1[7] --(1)--.
// >--> N5 --(1)--> D4[1]
// S2[4] --(3)--> D3[3] --(0)--' |
// ^ |
// \ /
// `------(0)-----'
//
// The shortest path is S0->D1->N5->D4, of distance 2 + 3 + 1 + 1 + 1 = 8.
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
graph.AddArc(NodeIndex(0), NodeIndex(1));
arc_lengths.push_back(3);
graph.AddArc(NodeIndex(2), NodeIndex(3));
arc_lengths.push_back(3);
graph.AddArc(NodeIndex(1), NodeIndex(5));
arc_lengths.push_back(1);
graph.AddArc(NodeIndex(3), NodeIndex(5));
arc_lengths.push_back(0);
graph.AddArc(NodeIndex(5), NodeIndex(3));
arc_lengths.push_back(0);
graph.AddArc(NodeIndex(5), NodeIndex(4));
arc_lengths.push_back(1);
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
// Repeat the same source and destination multiple times, to verify that
// it's supported.
std::vector<std::pair<NodeIndex, int>> sources = {{NodeIndex(0), 5},
{NodeIndex(2), 4},
{NodeIndex(0), 2},
{NodeIndex(0), 9}};
std::vector<std::pair<NodeIndex, int>> destinations = {{NodeIndex(1), 7},
{NodeIndex(4), 5},
{NodeIndex(3), 3},
{NodeIndex(4), 1},
{NodeIndex(4), 3}};
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/1,
/*distance_limit=*/1000),
Contains(NodeIndex(4)));
EXPECT_EQ(2 + 3 + 1 + 1, dijkstra.distances()[NodeIndex(4)]);
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(4)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(4)),
ElementsAre(/*0->1*/ ArcIndex(0), /*1->5*/ ArcIndex(2),
/*5->4*/ ArcIndex(5)));
EXPECT_EQ(2, dijkstra.GetSourceIndex(NodeIndex(0)));
EXPECT_EQ(3, dijkstra.GetDestinationIndex(NodeIndex(4)));
// Run it with a limit too small: it'll fail to discover any destination.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/2,
/*distance_limit=*/8), // Limit is exclusive.
IsEmpty());
// .. And with a limit that's just big enough for "4". It'll be ok.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/2,
/*distance_limit=*/9), // Limit is exclusive.
ElementsAre(NodeIndex(4)));
// Slightly modify the graph and try again. We want a case where the best
// destination isn't the one with the smallest distance offset.
destinations.push_back(
{NodeIndex(1), 2}); // D1 will be the closest destination now.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/1,
/*distance_limit=*/8), // Limit is exclusive.
ElementsAre(NodeIndex(1)));
EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1)));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)),
ElementsAre(/*0->1*/ ArcIndex(0)));
// Corner case: run with no destinations.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, {}, /*num_destinations_to_reach=*/99,
/*distance_limit=*/1000),
IsEmpty());
// Corner case: run with num_destinations_to_reach = 0.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/0,
/*distance_limit=*/1000),
IsEmpty());
// Call Get{Source,Destination}Index() on nodes that aren't sources or
// destinations. This returns junk; so we don't check the returned values,
// but we do check that it doesn't crash.
dijkstra.GetDestinationIndex(NodeIndex(4));
dijkstra.GetSourceIndex(NodeIndex(1));
// Setting num_reached_destinations=1 now should make '1' the only reachable
// destination, even if the limit is infinite.
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/1,
/*distance_limit=*/1000),
ElementsAre(NodeIndex(1)));
// Verify that if we set the number of destinations to infinity, they're all
// explored, and the search still stops before exploring the whole graph. To
// do that, we add one extra arc that's beyond the farthest destination's
// distance (including its destination offset), i.e. 1 (distance 2+3+7 = 12).
graph.AddArc(NodeIndex(5), NodeIndex(6));
arc_lengths.push_back(2);
graph.AddArc(NodeIndex(6), NodeIndex(7));
arc_lengths.push_back(0);
EXPECT_THAT(
dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations(
sources, destinations, /*num_destinations_to_reach=*/1000,
/*distance_limit=*/1000),
ElementsAre(NodeIndex(1), NodeIndex(4), NodeIndex(3)));
EXPECT_GE(dijkstra.distances()[NodeIndex(1)], 5);
EXPECT_GE(dijkstra.distances()[NodeIndex(4)], 7);
EXPECT_GE(dijkstra.distances()[NodeIndex(3)], 6);
// To verify that node #7 isn't reached, we can check its distance, which will
// still be set to the initialized "distance_limit - min_destination_offset".
EXPECT_GE(dijkstra.distances()[NodeIndex(7)], 1000 - 1);
}
TEST(BoundedDijkstraWrapperTest, OneToOneShortestPath) {
// Since we already tested the multiple sources - multiple destinations
// variant, we only need to test the "plumbing" here.
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
graph.AddArc(NodeIndex(0), NodeIndex(1));
arc_lengths.push_back(3);
graph.AddArc(NodeIndex(1), NodeIndex(2));
arc_lengths.push_back(2);
DijkstraWrapper<int> dijkstra(&graph, &arc_lengths);
EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(2), 6));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)),
ElementsAre(ArcIndex(0), ArcIndex(1)));
EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(0), 1));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre());
EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(1), NodeIndex(2), 3));
EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)), ElementsAre(ArcIndex(1)));
EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(2), 5));
EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(0), 0));
EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(1), NodeIndex(2), 2));
EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(2), NodeIndex(0), 1000));
}
TEST(BoundedDijkstraWrapperTest, CustomSettledNodeCallback) {
// A small chain: 8 --[3]--> 1 --[2]--> 42 --[3]--> 3 --[2]--> 4.
TestGraph graph;
DijkstraWrapper<int>::ByArc<int> arc_lengths;
graph.AddArc(NodeIndex(8), NodeIndex(1));
arc_lengths.push_back(3);
graph.AddArc(NodeIndex(1), NodeIndex(42));
arc_lengths.push_back(2);
graph.AddArc(NodeIndex(42), NodeIndex(3));
arc_lengths.push_back(3);
graph.AddArc(NodeIndex(3), NodeIndex(4));
arc_lengths.push_back(2);
typedef DijkstraWrapper<int> DijkstraType;
DijkstraType dijkstra(&graph, &arc_lengths);
// Tracks each NodeDistance it's called on, and sets the distance limit
// to 10 if it gets called on node 42.
std::vector<std::pair<NodeIndex, int>> settled_node_dists;
auto callback = [&settled_node_dists](NodeIndex node, int distance,
int* distance_limit) {
settled_node_dists.push_back({node, distance});
if (node == NodeIndex(42)) *distance_limit = 10;
};
EXPECT_THAT(
dijkstra.RunBoundedDijkstraWithSettledNodeCallback({{NodeIndex(8), 0}},
callback, 999),
ElementsAre(NodeIndex(8), NodeIndex(1), NodeIndex(42), NodeIndex(3)));
EXPECT_THAT(settled_node_dists,
ElementsAre(Pair(NodeIndex(8), 0), Pair(NodeIndex(1), 3),
Pair(NodeIndex(42), 5), Pair(NodeIndex(3), 8)));
}
TEST(BoundedDisjktraTest, RandomizedStressTest) {
std::mt19937 random;
const int kNumTests = 10'000;
constexpr int kint32max = std::numeric_limits<int>::max();
for (int test = 0; test < kNumTests; ++test) {
// Generate a random graph with random weights.
const NodeIndex num_nodes(absl::Uniform(random, 1, 12));
const ArcIndex num_arcs(absl::Uniform(
absl::IntervalClosed, random, 0,
std::min(num_nodes.value() * (num_nodes.value() - 1), 15)));
TestGraph graph(num_nodes, num_arcs);
for (ArcIndex a(0); a < num_arcs; ++a) {
graph.AddArc(NodeIndex(absl::Uniform(random, 0, num_nodes.value())),
NodeIndex(absl::Uniform(random, 0, num_nodes.value())));
}
DijkstraWrapper<int>::ByArc<int> lengths(num_arcs);
for (int& w : lengths) w = absl::Uniform(random, 0, 5);
// Run Floyd-Warshall as a 'reference' shortest path algorithm.
FlatMatrix<int> ref_dist(num_nodes.value(), num_nodes.value(), kint32max);
for (ArcIndex a(0); a < num_arcs; ++a) {
int& d = ref_dist[graph.Tail(a).value()][graph.Head(a).value()];
if (lengths[a] < d) d = lengths[a];
}
for (NodeIndex node(0); node < num_nodes; ++node) {
ref_dist[node.value()][node.value()] = 0;
}
for (NodeIndex k(0); k < num_nodes; ++k) {
for (NodeIndex i(0); i < num_nodes; ++i) {
for (NodeIndex j(0); j < num_nodes; ++j) {
const int64_t dist_through_k =
static_cast<int64_t>(ref_dist[i.value()][k.value()]) +
ref_dist[k.value()][j.value()];
if (dist_through_k < ref_dist[i.value()][j.value()])
ref_dist[i.value()][j.value()] = dist_through_k;
}
}
}
// Compute the graph's largest distance below kint32max.
int max_distance = 0;
for (NodeIndex i(0); i < num_nodes; ++i) {
for (NodeIndex j(0); j < num_nodes; ++j) {
const int d = ref_dist[i.value()][j.value()];
if (d != kint32max && d > max_distance) max_distance = d;
}
}
// Now, run some Dijkstras and verify that they match. To balance out the
// FW (Floyd-Warshall) which is O(N³), we run more than one Dijkstra per FW.
DijkstraWrapper<int> dijkstra(&graph, &lengths);
for (int num_dijkstra = 0; num_dijkstra < 20; ++num_dijkstra) {
// Draw the distance limit.
const int limit =
absl::Bernoulli(random, 0.2)
? kint32max
: absl::Uniform(absl::IntervalClosed, random, 0, max_distance);
// Draw sources (*with* repetition) with initial distances.
const int num_sources = absl::Uniform(random, 1, 5);
std::vector<std::pair<NodeIndex, int>> sources(num_sources);
for (auto& [s, dist] : sources) {
s = NodeIndex(absl::Uniform(random, 0, num_nodes.value()));
dist = absl::Uniform(absl::IntervalClosed, random, 0, max_distance + 1);
}
// Precompute the reference minimum distance to each node (using any of
// the sources), and the expected reached nodes: any node whose distance
// is < limit. That includes the sources: if a source's initial distance
// is ≥ limit, it won't be reached.That includes the source themselves.
DijkstraWrapper<int>::ByNode<int> node_min_dist(num_nodes, kint32max);
DijkstraWrapper<int>::ByNode<NodeIndex> expected_reached_nodes;
for (NodeIndex node(0); node < num_nodes; ++node) {
int min_dist = kint32max;
for (const auto& [src, dist] : sources) {
// Cast to int64_t to avoid overflows.
min_dist = std::min<int64_t>(
min_dist,
static_cast<int64_t>(ref_dist[src.value()][node.value()]) + dist);
}
node_min_dist[node] = min_dist;
if (min_dist < limit) expected_reached_nodes.push_back(node);
}
const auto reached_nodes =
dijkstra.RunBoundedDijkstraFromMultipleSources(sources, limit);
EXPECT_THAT(reached_nodes,
UnorderedElementsAreArray(expected_reached_nodes));
for (const NodeIndex node : reached_nodes) {
EXPECT_EQ(dijkstra.distances()[node], node_min_dist[node]) << node;
}
ASSERT_FALSE(HasFailure())
<< DUMP_VARS(num_nodes, num_arcs, num_sources, limit, lengths)
<< "\n With graph:\n"
<< util::GraphToString(graph, util::PRINT_GRAPH_ARCS);
}
}
}
template <bool arc_lengths_are_discrete>
void BM_GridGraph(benchmark::State& state) {
typedef util::StaticGraph<int> Graph;
const int64_t kWidth = 100;
const int64_t kHeight = 10000;
const int kSourceNode = static_cast<int>(kWidth * kHeight / 2);
std::unique_ptr<Graph> graph =
util::Create2DGridGraph<Graph>(/*width=*/kWidth, /*height=*/kHeight);
BoundedDijkstraWrapper<Graph, int64_t>::ByArc<int64_t> arc_lengths(
graph->num_arcs(), 0);
const int64_t min_length = arc_lengths_are_discrete ? 0 : 1;
const int64_t max_length = arc_lengths_are_discrete ? 2 : 1000000000000000L;
std::mt19937 random(12345);
for (int64_t& length : arc_lengths) {
length = absl::Uniform(random, min_length, max_length + 1);
}
BoundedDijkstraWrapper<Graph, int64_t> dijkstra(graph.get(), &arc_lengths);
const int64_t kSearchRadius = kWidth * (min_length + max_length) / 2;
// NOTE(user): The expected number of nodes visited is in ϴ(kWidth²),
// since the search radius is ϴ(kWidth). The exact constant is hard to
// derive mathematically as a function of the radius formula, so I measured
// it empirically and it was close to 0.5, which seemed about right.
const int64_t kExpectedApproximateSearchSize = kWidth * kWidth / 2;
int64_t total_nodes_visited = 0;
for (auto _ : state) {
const int num_nodes_visited =
dijkstra
.RunBoundedDijkstra(/*source_node=*/kSourceNode,
/*distance_limit=*/kSearchRadius)
.size();
// We verify that the Dijkstra search size is in the expected range, to
// make sure that we're measuring what we want in this benchmark.
CHECK_GE(num_nodes_visited, kExpectedApproximateSearchSize / 2);
CHECK_GE(num_nodes_visited, kExpectedApproximateSearchSize * 2);
total_nodes_visited += num_nodes_visited;
}
state.SetItemsProcessed(total_nodes_visited);
}
BENCHMARK(BM_GridGraph<true>);
BENCHMARK(BM_GridGraph<false>);
} // namespace
} // namespace operations_research