Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
binary_search.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#ifndef OR_TOOLS_ALGORITHMS_BINARY_SEARCH_H_
15#define OR_TOOLS_ALGORITHMS_BINARY_SEARCH_H_
16
17#include <cmath>
18#include <cstdint>
19#include <functional>
20#include <utility>
21
22#include "absl/functional/function_ref.h"
23#include "absl/log/check.h"
24#include "absl/numeric/int128.h"
25#include "absl/types/span.h"
27
28namespace operations_research {
29
30// EXAMPLE:
31// // Finds the value x in [0,Pi/2] such that cos(x)=2sin(x):
32// BinarySearch<double>(/*x_true=*/0.0, /*x_false=*/M_PI/2,
33// [](double x) { return cos(x) >= 2*sin(x); });
34//
35// Note that x_true > x_false is supported: it works either way.
36//
37// Ideally, f is a monotonic boolean function, such that:
38// - f(x_true) = true
39// - f(x_false) = false
40// - there exists X such that f(x)=true for all x between x_true and X, and
41// f(x)=false for all x between X and x_false.
42//
43// In those conditions, this returns that value X (note that f(X) is true).
44// See below for the NON-MONOTONIC case.
45//
46// Also note that 'Point' may be floating-point types: the function will still
47// converge when the midpoint can't be distinguished from one of the limits,
48// which will always happen.
49// You can use other types than numerical types, too. absl::Duration is
50// naturally supported.
51//
52// OVERFLOWS and NON-NUMERICAL TYPES: If your points may be subject to overflow
53// (e.g. ((kint32max-1) + (kint32max))/2 will overflow an int32_t), or they
54// don't support doing (x+y)/2, you can specialize BinarySearchMidpoint() to
55// suit your needs. See the examples in the unit test.
56//
57// NON-MONOTONIC FUNCTIONS: If f isn't monotonic, the binary search will still
58// run with it typical complexity, and finish. The Point X that it returns
59// will be a "local" inflexion point, meaning that the smallest possible move
60// of that point X to a point X' (in the x_true->x_false direction deduced from
61// the arguments) would make f(X') return false. EXAMPLES:
62// - If Point=int32_t, then the returned X verifies f(X) = true and f(X') =
63// false
64// with X' = X + (x_true < x_false ? 1 : -1).
65// - If Point=double, ditto with X' = X * (1 + (x_true < x_false ? ε : -ε)),
66// where ε = std::numeric_limits<double>::epsilon().
67//
68// Note also that even if f() is non-deterministic, i.e. f(X) can sometimes
69// return true and sometimes false for the same X, then the binary search will
70// still finish, but it's hard to say anything about the returned X.
71template <class Point>
72Point BinarySearch(Point x_true, Point x_false, std::function<bool(Point)> f);
73
74// Used by BinarySearch(). This is just (x+y)/2, with some DCHECKs to catch
75// overflows. You should override (i.e. specialize) it if you risk getting
76// overflows or need something more complicated than (x+y)/2.
77// See examples in the unit test.
78template <class Point>
79Point BinarySearchMidpoint(Point x, Point y);
80
81// Returns the minimum of a convex function on a discrete set of sorted points.
82// It is an error to call this with an empty set of points.
83//
84// We assume the function is "unimodal" with potentially more than one minimum.
85// That is strictly decreasing, then have a minimum that can have many points,
86// and then is strictly increasing. In this case if we have two points with
87// exactly the same value, one of the minimum is always between the two. We will
88// only return one of the minimum.
89//
90// Note that if we allow for non strictly decreasing/increasing, then you have
91// corner cases where one need to check all points to find the minimum. For
92// instance, if the function is constant except at one point where it is lower.
93//
94// The usual algorithm to optimize such a function is a ternary search. However
95// here we assume calls to f() are expensive, and we try to minimize those. So
96// we use a slightly different algorithm than:
97// https://en.wikipedia.org/wiki/Ternary_search
98//
99// TODO(user): Some relevant optimizations:
100// - Abort early if we know a lower bound on the min.
101// - Seed with a starting point if we know one.
102// - We technically do not need the points to be sorted and can use
103// linear-time median computation to speed this up.
104//
105// TODO(user): replace std::function by absl::FunctionRef here and in
106// BinarySearch().
107template <class Point, class Value>
108std::pair<Point, Value> ConvexMinimum(absl::Span<const Point> sorted_points,
109 std::function<Value(Point)> f);
110
111// Internal part of ConvexMinimum() that can also be used directly in some
112// situation when we already know some value of f(). This assumes that we
113// already have a current_min candidate that is either before or after all the
114// points in sorted_points.
115template <class Point, class Value>
116std::pair<Point, Value> ConvexMinimum(bool is_to_the_right,
117 std::pair<Point, Value> current_min,
118 absl::Span<const Point> sorted_points,
119 std::function<Value(Point)> f);
120
121// Searches in the range [begin, end), where Point supports basic arithmetic.
122template <class Point, class Value>
123std::pair<Point, Value> RangeConvexMinimum(Point begin, Point end,
124 absl::FunctionRef<Value(Point)> f);
125template <class Point, class Value>
126std::pair<Point, Value> RangeConvexMinimum(std::pair<Point, Value> current_min,
127 Point begin, Point end,
128 absl::FunctionRef<Value(Point)> f);
129
130// _____________________________________________________________________________
131// Implementation.
132
133namespace internal {
134template <class T>
135bool IsNanGeneric(const T&) {
136 return false;
137}
138
139template <>
140inline bool IsNanGeneric(const float& x) {
141 return std::isnan(x);
142}
143template <>
144inline bool IsNanGeneric(const double& x) {
145 return std::isnan(x);
146}
147template <>
148inline bool IsNanGeneric(const long double& x) {
149 return std::isnan(x);
150}
151
152template <typename T>
153bool AreNumbersOfSameSign(const T& x, const T& y) {
154 if constexpr (std::is_same_v<T, absl::int128>) {
155 return (x < 0) == (y < 0);
156 } else if constexpr (std::is_same_v<T, absl::uint128> ||
157 std::is_unsigned_v<T>) {
158 return true;
159 } else if constexpr (std::is_signed_v<T>) {
160 return std::signbit(x) == std::signbit(y);
161 }
162 return false;
163}
164
165template <typename IntT>
166constexpr bool UsesTwosComplement() {
167 if constexpr (std::is_integral_v<IntT>) {
168 if constexpr (std::is_unsigned_v<IntT>) {
169 return true;
170 } else {
171 return (IntT{-1} & IntT{3}) == IntT{3};
172 }
173 }
174 return false;
175}
176
177} // namespace internal
178
179template <class Point>
180Point BinarySearchMidpoint(Point x, Point y) {
181 Point midpoint;
182 if constexpr (internal::UsesTwosComplement<Point>()) {
183 // For integers using two's complement (all compilers in practice up to
184 // c++17, and all compilers in theory starting from c++20), we can use a
185 // trick from Hacker's delight. See e.g.
186 // https://lemire.me/blog/2022/12/06/fast-midpoint-between-two-integers-without-overflow/
187 midpoint = (x | y) - ((x ^ y) >> 1);
188 } else {
190 ? (x + y) / 2
191 :
192 // For integers of the same sign, avoid overflows with a
193 // simple trick.
194 x < y ? x + (y - x) / 2
195 : y + (x - y) / 2;
196 }
197 if (DEBUG_MODE &&
198 !internal::IsNanGeneric(midpoint)) { // Check for overflows.
199 if (x < y) {
200 DCHECK_LE(midpoint, y) << "Overflow?";
201 DCHECK_GE(midpoint, x) << "Overflow?";
202 } else {
203 DCHECK_GE(midpoint, y) << "Overflow?";
204 DCHECK_LE(midpoint, x) << "Overflow?";
205 }
206 }
207 return midpoint;
208}
209
210template <class Point>
211Point BinarySearch(Point x_true, Point x_false, std::function<bool(Point)> f) {
212 DCHECK(f(x_true)) << x_true;
213 DCHECK(!f(x_false)) << x_false;
214 int num_iterations = 0;
215 constexpr int kMaxNumIterations = 1000000;
216 while (true) {
217 // NOTE(user): If your "Point" type doesn't support the + or the /2
218 // operations, we could imagine using the same trick as IsNanGeneric() to
219 // make BinarySearch() work for your use case.
220 const Point x = BinarySearchMidpoint(x_true, x_false);
221 if (internal::IsNanGeneric(x) || x == x_true || x == x_false) return x_true;
222 if (++num_iterations > kMaxNumIterations) {
223 LOG(DFATAL)
224 << "The binary search seems to loop forever! This indicates that your"
225 " input types don't converge when repeatedly taking the midpoint";
226 return x_true;
227 }
228 if (f(x)) {
229 x_true = x;
230 } else {
231 x_false = x;
232 }
233 }
234}
235
236template <class Point, class Value>
237std::pair<Point, Value> RangeConvexMinimum(Point begin, Point end,
238 absl::FunctionRef<Value(Point)> f) {
239 DCHECK_LT(begin, end);
240 const Value size = end - begin;
241 if (size == 1) {
242 return {begin, f(begin)};
243 }
244
245 // Starts by splitting interval in two with two queries and getting some info.
246 // Note the current min will be outside the interval.
247 std::pair<Point, Value> current_min;
248 {
249 DCHECK_GE(size, 2);
250 const Point mid = begin + (end - begin) / 2;
251 DCHECK_GT(mid, begin);
252 const Value v = f(mid);
253 const Point before_mid = mid - 1;
254 const Value before_v = f(before_mid);
255 if (before_v == v) return {before_mid, before_v};
256 if (before_v < v) {
257 // Note that we exclude before_mid from the range.
258 current_min = {before_mid, before_v};
259 end = before_mid;
260 } else {
261 current_min = {mid, v};
262 begin = mid + 1;
263 }
264 }
265 if (begin >= end) return current_min;
266 return RangeConvexMinimum<Point, Value>(current_min, begin, end, f);
267}
268
269template <class Point, class Value>
270std::pair<Point, Value> RangeConvexMinimum(std::pair<Point, Value> current_min,
271 Point begin, Point end,
272 absl::FunctionRef<Value(Point)> f) {
273 DCHECK_LT(begin, end);
274 while ((end - begin) > 1) {
275 DCHECK(current_min.first < begin || current_min.first >= end);
276 bool current_is_after_end = current_min.first >= end;
277 const Point mid = begin + (end - begin) / 2;
278 const Value v = f(mid);
279 if (v >= current_min.second) {
280 // If the midpoint is no better than our current minimum, then the
281 // global min must lie between our midpoint and our current min.
282 if (current_is_after_end) {
283 begin = mid + 1;
284 } else {
285 end = mid;
286 }
287 } else {
288 // v < current_min.second, we cannot decide, so we use a second value
289 // close to v like in the initial step.
290 DCHECK_GT(mid, begin);
291 const Point before_mid = mid - 1;
292 const Value before_v = f(before_mid);
293 if (before_v == v) return {before_mid, before_v};
294 if (before_v < v) {
295 current_min = {before_mid, before_v};
296 current_is_after_end = true;
297 end = before_mid;
298 } else {
299 current_is_after_end = false;
300 current_min = {mid, v};
301 begin = mid + 1;
302 }
303 }
304 }
305
306 if (end - begin == 1) {
307 const Value v = f(begin);
308 if (v <= current_min.second) return {begin, v};
309 }
310 return current_min;
311}
312
313template <class Point, class Value>
314std::pair<Point, Value> ConvexMinimum(absl::Span<const Point> sorted_points,
315 std::function<Value(Point)> f) {
316 auto index_f = [&](int index) -> Value { return f(sorted_points[index]); };
317 const auto& [index, v] =
318 RangeConvexMinimum<int64_t, Value>(0, sorted_points.size(), index_f);
319 return {sorted_points[index], v};
320}
321
322template <class Point, class Value>
323std::pair<Point, Value> ConvexMinimum(bool is_to_the_right,
324 std::pair<Point, Value> current_min,
325 absl::Span<const Point> sorted_points,
326 std::function<Value(Point)> f) {
327 auto index_f = [&](int index) -> Value { return f(sorted_points[index]); };
328 std::pair<int, Value> index_current_min = std::make_pair(
329 is_to_the_right ? sorted_points.size() : -1, current_min.second);
330 const auto& [index, v] = RangeConvexMinimum<int64_t, Value>(
331 index_current_min, 0, sorted_points.size(), index_f);
332 if (index == index_current_min.first) return current_min;
333 return {sorted_points[index], v};
334}
335} // namespace operations_research
336
337#endif // OR_TOOLS_ALGORITHMS_BINARY_SEARCH_H_
IntegerValue y
IntegerValue size
int index
const bool DEBUG_MODE
Definition macros.h:24
bool AreNumbersOfSameSign(const T &x, const T &y)
constexpr bool UsesTwosComplement()
std::function< int64_t(const Model &)> Value(IntegerVariable v)
This checks that the variable is fixed.
Definition integer.h:1975
In SWIG mode, we don't want anything besides these top-level includes.
Point BinarySearchMidpoint(Point x, Point y)
std::pair< Point, Value > RangeConvexMinimum(Point begin, Point end, absl::FunctionRef< Value(Point)> f)
Searches in the range [begin, end), where Point supports basic arithmetic.
Point BinarySearch(Point x_true, Point x_false, std::function< bool(Point)> f)
std::pair< Point, Value > ConvexMinimum(absl::Span< const Point > sorted_points, std::function< Value(Point)> f)
const Variable x
Definition qp_tests.cc:127
std::optional< int64_t > end