// 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/algorithms/binary_search.h" #include #include #include #include #include #include #include #include "absl/base/log_severity.h" #include "absl/numeric/int128.h" #include "absl/random/distributions.h" #include "absl/random/random.h" #include "absl/strings/str_format.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "benchmark/benchmark.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/hash.h" namespace operations_research { // Correctly picking the midpoint of two integers in all cases isn't trivial! template <> inline int BinarySearchMidpoint(int x, int y) { if (x > y) std::swap(x, y); if (x >= 0 || y < 0) return x + (y - x) / 2; return (x + y) / 2; } namespace { TEST(BinarySearchTest, DoubleExample) { // M_PI is problematic on windows. // std::numbers::pi is C++20 (incompatible with OR-Tools). const double kPi = 3.14159265358979323846; const double x = BinarySearch(/*x_true=*/0.0, /*x_false=*/kPi / 2, [](double x) { return cos(x) >= 2 * sin(x); }); EXPECT_GE(x, 0); EXPECT_LE(x, kPi / 2); EXPECT_EQ(cos(x), 2 * sin(x)) << x; } template class BinarySearchIntTest : public ::testing::Test {}; TYPED_TEST_SUITE_P(BinarySearchIntTest); TYPED_TEST_P(BinarySearchIntTest, IntExampleWithReversedIntervalOrder) { EXPECT_EQ( BinarySearch(/*x_true=*/67, /*x_false=*/23, [](TypeParam x) { return x > TypeParam{42}; }), 43); } TYPED_TEST_P(BinarySearchIntTest, IntOverflowStressTest) { const TypeParam kBounds[] = {std::numeric_limits::min(), std::numeric_limits::min() + 1, std::numeric_limits::min() + 2, std::numeric_limits::min() + 3, 0, 1, 2, 3, std::numeric_limits::max() - 3, std::numeric_limits::max() - 2, std::numeric_limits::max() - 1, std::numeric_limits::max()}; for (TypeParam x : kBounds) { for (TypeParam y : kBounds) { if (x == y) continue; ASSERT_EQ(BinarySearch(/*x_true=*/x, /*x_false=*/y, [x](TypeParam t) { return t == x; }), x) << "x=" << x << ", y=" << y; } } } REGISTER_TYPED_TEST_SUITE_P(BinarySearchIntTest, IntExampleWithReversedIntervalOrder, IntOverflowStressTest); using MyTypes = ::testing::Types; INSTANTIATE_TYPED_TEST_SUITE_P(My, BinarySearchIntTest, MyTypes); TEST(BinarySearchTest, LargeInt128SearchDomain) { absl::int128 target = -1234567890123456789; target <<= 50; // Make sure it does need more than 64 or even 96 bits. EXPECT_EQ(BinarySearch( /*x_true=*/std::numeric_limits::min(), /*x_false=*/std::numeric_limits::max(), [target](absl::int128 x) { return x < target; }), target - 1); } TEST(BinarySearchTest, VeryLongDoubleSearchDomain) { // Binary search for the smallest possible long double that is > 0, // starting with interval [0, numeric_limit::max()]. This is probably close to // the longest possible binary search on a widely-available numerical type. EXPECT_EQ(BinarySearch( /*x_true=*/std::numeric_limits::max(), /*x_false=*/0.0, [](long double x) { return x > 0; }), std::numeric_limits::denorm_min()); } TEST(BinarySearchTest, InfinityCornerCases) { constexpr double kInfinity = std::numeric_limits::infinity(); EXPECT_THAT(BinarySearch( /*x_true=*/-kInfinity, /*x_false=*/kInfinity, [](double x) { return x < 0; }), -kInfinity); EXPECT_EQ(BinarySearch( /*x_true=*/-1, /*x_false=*/kInfinity, [](double x) { return x < 0; }), -1); EXPECT_THAT(BinarySearch( /*x_true=*/kInfinity, /*x_false=*/0, [](double x) { return x > 0; }), kInfinity); } TEST(BinarySearchTest, NanCornerCases) { EXPECT_THAT(BinarySearch( /*x_true=*/std::numeric_limits::quiet_NaN(), /*x_false=*/0, [](double x) { return !(x == 0); }), testing::IsNan()); EXPECT_EQ(BinarySearch( /*x_true=*/0, /*x_false=*/std::numeric_limits::quiet_NaN(), [](double x) { return x == 0; }), 0); } TEST(BinarySearchTest, WithAbslDuration) { EXPECT_THAT(BinarySearch( /*x_true=*/absl::Hours(100000), /*x_false=*/absl::ZeroDuration(), [](absl::Duration x) { return x > absl::ZeroDuration(); }), // Smallest non-zero absl::Duration. absl::Nanoseconds(0.25)); EXPECT_EQ(BinarySearch( /*x_true=*/absl::InfiniteDuration(), /*x_false=*/-absl::Seconds(100), [](absl::Duration t) { return t > absl::Seconds(1); }), absl::InfiniteDuration()); } TEST(BinarySearchDeathTest, DiesIfEitherBoundaryConditionViolatedInFastbuild) { if (!DEBUG_MODE) GTEST_SKIP(); EXPECT_DEATH(BinarySearch(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 999; }), ""); EXPECT_DEATH(BinarySearch(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 0; }), ""); EXPECT_DEATH(BinarySearch(/*x_true=*/0, /*x_false=*/42, [](int x) { return x > 20; }), ""); } TEST(BinarySearchDeathTest, DiesIfBoundCheckIsEnabledAndABoundIsViolated) { // EXPECT_DEATH does not work with 2-parameter templates. auto bs = [](int x_true, int x_false, auto f) { return BinarySearch(x_true, x_false, f); }; EXPECT_DEATH(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 999; }), ""); EXPECT_DEATH(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 0; }), ""); EXPECT_DEATH(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x > 20; }), ""); } TEST(BinarySearchTest, NoDeathIfBoundCheckIsDisabled) { // EXPECT_EQ does not work with 2-parameter templates. auto bs = [](int x_true, int x_false, auto f) { return BinarySearch(x_true, x_false, f); }; // f is true on the whole interval ]0, 42[: return last value 41. EXPECT_EQ(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 999; }), 41); // f is true on reversed interval ]42, 0[: return last value 1. EXPECT_EQ(bs(/*x_true=*/42, /*x_false=*/0, [](int x) { return x < 999; }), 1); // f is false on the whole interval ]0, 42[: return x_true bound 0. EXPECT_EQ(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 0; }), 0); // f is false on reversed interval ]42, 0[: return x_true bound 42. EXPECT_EQ(bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x < 0; }), 0); // f is false on x_true, true on x_false. // No DCHECK trigger, instead return a transition point of the function // f': x_true -> true, x_false -> false, x_true < x < x_false -> f(x). // The transitions are: 0 (true) -> 1 (false) and 41 (true) -> 42 (false). { const int x_transition = bs(/*x_true=*/0, /*x_false=*/42, [](int x) { return x > 20; }); EXPECT_TRUE(x_transition == 0 || x_transition == 41); } // f is false on x_true, true on x_false, with a reversed interval. // No DCHECK trigger, return one of the transition points. // The transitions are: 42 (true) -> 41 (false) and 1 (true) -> 0 (false). { const int x_transition = bs(/*x_true=*/42, /*x_false=*/0, [](int x) { return x < 20; }); EXPECT_TRUE(x_transition == 42 || x_transition == 1); } } } // namespace // Examples of cases where one needs to override BinarySearchMidpoint() to get // correct results. // Note that template specializations must be exactly in the same namespace, // hence the presence of these tests outside the unnamed namespace. template <> inline absl::Time BinarySearchMidpoint(absl::Time x, absl::Time y) { return x + (y - x) / 2; } TEST(BinarySearchTest, WithAbslTime) { const absl::Time t0 = absl::Now(); EXPECT_EQ(BinarySearch( /*x_true=*/t0 + absl::Hours(1), /*x_false=*/t0, [t0](absl::Time x) { return x > t0; }), t0 + absl::Nanoseconds(0.25)); EXPECT_EQ(BinarySearch( /*x_true=*/absl::InfinitePast(), /*x_false=*/absl::Now() + absl::Seconds(100), [](absl::Time x) { return x < absl::Now(); }), absl::InfinitePast()); } TEST(BinarySearchTest, NonMonoticPredicateReachesLocalInflexionPoint_Double) { absl::BitGen random; auto generate_random_double = [&random]() { // We generate the sign, mantissa and exponent separately. return (absl::Bernoulli(random, 0.5) ? 1 : -1) * scalbn(absl::Uniform(random, 1, 2), absl::Uniform(random, -1023, 1023)); }; constexpr double kEps = std::numeric_limits::epsilon(); const int kNumAttempts = 100000; for (int attempt = 0; attempt < kNumAttempts; ++attempt) { const uint64_t hash_seed = random(); std::function non_monotonic_predicate = [hash_seed](double x) { return fasthash64(reinterpret_cast(&x), sizeof(x), hash_seed) & 1; }; // Pick a random [x_true, x_false] interval which verifies f(x_true) = true // and f(x_false) = false. double x_true, x_false; do { x_true = generate_random_double(); } while (!non_monotonic_predicate(x_true)); // x_false will either be set to a another random double, or to a small // perturbation from x_true. if (absl::Bernoulli(random, 0.5)) { // random double. do { x_false = generate_random_double(); } while (non_monotonic_predicate(x_false)); } else { // small perturbation from x_true. do { const double eps = absl::LogUniform(random, 1, 1000) * kEps; x_false = x_true * (1 + (absl::Bernoulli(random, 0.5) ? eps : -eps)); } while (non_monotonic_predicate(x_false)); } ASSERT_NE(x_true, x_false); // Verify that our predicate is deterministic. for (int i = 0; i < 20; ++i) { ASSERT_TRUE(non_monotonic_predicate(x_true)); } for (int i = 0; i < 20; ++i) { ASSERT_FALSE(non_monotonic_predicate(x_false)); } // Perform the binary search. const double solution = BinarySearch(x_true, x_false, non_monotonic_predicate); SCOPED_TRACE(absl::StrFormat("x_true=%.16g, x_false=%.16g, solution=%.16g", x_true, x_false, solution)); // Verify that the solution is in [x_true, x_false[. if (x_true < x_false) { ASSERT_GE(solution, x_true); ASSERT_LT(solution, x_false); } else { ASSERT_LE(solution, x_true); ASSERT_GT(solution, x_false); } // Verify that f(solution')=false, where solution' is the smallest double // "after" solution (in the x_true->x_false direction). ASSERT_FALSE(non_monotonic_predicate(std::nextafter(solution, x_false))); } } TEST(BinarySearchTest, NonDeterministicPredicateStillConverges) { if (DEBUG_MODE) { GTEST_SKIP() << "DCHECKs catch f(x_true)=false or f(x_false)=true."; } absl::BitGen random; std::function random_predicate = [&random](int) { return absl::Bernoulli(random, 0.5); }; const int kNumAttempts = 100000; for (int attempt = 0; attempt < kNumAttempts; ++attempt) { const int x_true = random(); const int x_false = absl::Bernoulli(random, 0.5) ? x_true + (absl::Bernoulli(random, 0.5) ? 1 : -1) * absl::LogUniform(random, 0, 1000) : random(); const int solution = BinarySearch(x_true, x_false, random_predicate); SCOPED_TRACE(absl::StrFormat("x_true=%d, x_false=%d, solution=%d", x_true, x_false, solution)); if (x_false == x_true) { ASSERT_EQ(solution, x_true); } else if (x_true < x_false) { ASSERT_GE(solution, x_true); ASSERT_LT(solution, x_false); } else { ASSERT_LE(solution, x_true); ASSERT_GT(solution, x_false); } } } template void BM_BinarySearch(benchmark::State& state) { auto functor = [](T x) { return x > std::numeric_limits::max() / 2; }; for (const auto s : state) { benchmark::DoNotOptimize(functor); auto result = BinarySearch(std::numeric_limits::max(), std::numeric_limits::min(), functor); benchmark::DoNotOptimize(result); } } BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); BENCHMARK(BM_BinarySearch); TEST(ConvexMinimumTest, ExhaustiveTest) { const int n = 99; std::vector points(n); std::vector values(n); for (int i = 0; i < n; ++i) points[i] = i; int total_num_queries = 0; int max_num_queries = 0; for (int b1 = 0; b1 < n; ++b1) { for (int i = b1; i >= 0; --i) values[i] = b1 - i; for (int b2 = b1; b2 < n; ++b2) { for (int i = b2; i < n; ++i) values[i] = i - b2; int num_queries = 0; const auto [point, value] = ConvexMinimum(points, [&](int p) { ++num_queries; return values[p]; }); total_num_queries += num_queries; max_num_queries = std::max(max_num_queries, num_queries); EXPECT_EQ(value, 0); EXPECT_GE(point, b1); EXPECT_LE(point, b2); // Fail after one example. ASSERT_TRUE(value == 0 && b1 <= point && point <= b2) << "queries: " << num_queries << " opt range: [" << b1 << ", " << b2 << "]"; } } // TODO(user): we can probably do better. EXPECT_EQ(total_num_queries, 19376); EXPECT_EQ(max_num_queries, 12); } TEST(ConvexMinimumTest, OneQueryIfSizeOne) { std::vector points{0}; std::vector values{0.0}; int num_queries = 0; const auto [point, value] = ConvexMinimum(points, [&](int p) { ++num_queries; return values[p]; }); EXPECT_EQ(point, 0); EXPECT_EQ(value, 0.0); EXPECT_EQ(num_queries, 1); } TEST(ConvexMinimumTest, TwoQueriesIfSizeTwo) { std::vector points{0, 1}; std::vector values{0.0, 1.0}; int num_queries = 0; const auto [point, value] = ConvexMinimum(points, [&](int p) { ++num_queries; return values[p]; }); EXPECT_EQ(point, 0); EXPECT_EQ(value, 0.0); EXPECT_EQ(num_queries, 2); } TEST(ConvexMinimumTest, TwoQueriesIfSizeTwoReversed) { std::vector points{0, 1}; std::vector values{1.0, 0.0}; int num_queries = 0; const auto [point, value] = ConvexMinimum(points, [&](int p) { ++num_queries; return values[p]; }); EXPECT_EQ(point, 1); EXPECT_EQ(value, 0.0); EXPECT_EQ(num_queries, 2); } TEST(RangeConvexMinimumTest, HugeRangeTest) { int total_num_queries = 0; int max_num_queries = 0; for (int b1 = -100; b1 < 100; ++b1) { for (int b2 = b1; b2 < b1 + 100; ++b2) { int num_queries = 0; const auto [point, value] = RangeConvexMinimum( std::numeric_limits::min() / 2, std::numeric_limits::max() / 2, [&](int64_t v) -> double { ++num_queries; if (v < b1) { return b1 - v; } else if (v > b2) { return v - b2; } return 0; }); total_num_queries += num_queries; max_num_queries = std::max(max_num_queries, num_queries); EXPECT_EQ(value, 0); EXPECT_GE(point, b1); EXPECT_LE(point, b2); // Don't continue past the first failing example to limit the number of // errors. ASSERT_TRUE(value == 0 && b1 <= point && point <= b2) << "queries: " << num_queries << " opt range: [" << b1 << ", " << b2 << "]"; } } // 80 is the worst case we would expect from ternary search: 2*log_3(2^63). EXPECT_LE(max_num_queries, 80); } } // namespace operations_research