Binary Search

This page briefly introduces binary search, ternary search derived from binary search, and binary search on the answer.

Definition

Binary search (also called half-interval search or logarithmic search) is an algorithm for finding an element in a sorted array.

Process

Consider finding a number in an ascending array.

Each time, binary search examines the middle element of the current part of the array. If the middle element is exactly the one being sought, the search ends. If the middle element is smaller than the target value, everything on its left is even smaller, so the target cannot be there and it is enough to search the right side. If the middle element is larger than the target, the same reasoning means that only the left side needs to be searched.

Properties

Time Complexity

The best-case time complexity of binary search is O(1)O(1).

The average-case and worst-case time complexities of binary search are both O(log⁡n)O(\log n). During the search, the algorithm halves the query interval each time, so an array of length nn requires at most O(log⁡n)O(\log n) searches.

Space Complexity

The iterative version of binary search has space complexity O(1)O(1).

The recursive version of binary search (without tail-call elimination) has space complexity O(log⁡n)O(\log n).

Implementation

int binary_search(int start, int end, int key) {
  int ret = -1;  // Return index -1 if the data is not found
  int mid;
  while (start <= end) {
    mid = start + ((end - start) >> 1);  // A direct average may overflow, so use this formula
    if (arr[mid] < key)
      start = mid + 1;
    else if (arr[mid] > key)
      end = mid - 1;
    else {  // Check equality last because most searches satisfy either greater-than or less-than
      ret = mid;
      break;
    }
  }
  return ret;  // Single exit point
}
Note

See Compiler optimizations #Replacing multiplication with shifts. When nn is a signed number and you can guarantee that n≥0n\ge 0, n >> 1 uses fewer instructions than n / 2.

Minimize the Maximum

Note that “sorted” is used here in a generalized sense. If all elements on the left or all elements on the right side of an array satisfy some condition, while all elements on the other side do not, this can also be regarded as sorted (if satisfying the condition is represented by 11 and not satisfying it by 00, it is sorted in this dimension for the condition at hand).In other words, binary search can be used to find the maximum (or minimum) value satisfying a condition.

To minimize the smallest possible value of the maximum satisfying a condition (minimize the maximum), the first idea is to enumerate the candidate “maximum” from small to large and then check whether it is valid. If the answer is monotonic, binary search can find it more quickly. Therefore, to solve this kind of “minimize the maximum” problem with binary search, the following three conditions must hold:

  1. The answer lies in a fixed interval.
  2. Finding a value that satisfies the condition may not be easy, but it must be relatively easy to determine whether a given value satisfies the condition.
  3. The feasible solutions have a certain monotonicity over the interval. In other words, if xx satisfies the condition, then either x+1x + 1 or x−1x - 1 also satisfies it.(This gives the monotonicity mentioned above.)

Maximizing the minimum is analogous.

Binary Search in the STL

The C++ standard library provides std::lower_bound, which finds the first element not less than a given value, and std::upper_bound, which finds the first element greater than a given value. Both are defined in the <algorithm> header.

Both use binary search internally, so the elements must be sorted before they are called.

bsearch

The bsearch function is the C standard library's implementation of binary search and is defined in <stdlib.h>. In the C++ standard library, it is defined in <cstdlib>. qsort and bsearch are the only two algorithm-like functions in the C language.

Compared with the four parameters of qsort (STL related to sorting), the bsearch function adds the address of the element to be searched for at the far left. It is passed as an address so that the same comparison function as qsort can be reused directly, enabling an immediate search after sorting. Therefore, this parameter cannot be a value directly; the value to be searched for must first be stored in a variable, whose address is then passed.

Thus, bsearch has five parameters in total: the address of the element to be searched for, the array name, the number of elements, the element size, and the comparison rule. The comparison rule is still implemented by specifying a comparison function; see STL related to sorting for details.

The return value of bsearch is the address of the element found, and this address has type void.

Note that bsearch differs from the lower_bound and upper_bound above in two ways:

  • When multiple elements satisfy the condition, it returns the first such element encountered while performing the binary search, so this element may lie in the middle of the run of duplicate elements.
  • When the corresponding element cannot be found, it returns NULL.

lower_bound can implement exactly the same functionality as bsearch, so a problem solved with bsearch can also be solved by rewriting it directly with lower_bound. However, because of the second difference above, implementing lower_bound with bsearch is difficult in cases such as searching for 3 in the sequence 1, 2, 4, 5, 6.

Is it necessarily impossible to implement lower_bound with bsearch? No; there is a rather tricky technique. Taking advantage of the compiler's handling of the comparison function—its first parameter always points to the element being searched for, while its second parameter points to an element in the array being searched—we can also implement lower_bound and upper_bound with bsearch, as shown below. The searched array must be global so that its first address can be passed directly.

int A[100005];  // Example global array

// Find the address of the first element not less than the element being searched for
int lower(const void *p1, const void *p2) {
  int *a = (int *)p1;
  int *b = (int *)p2;
  if ((b == A || compare(a, b - 1) > 0) && compare(a, b) > 0)
    return 1;
  else if (b != A && compare(a, b - 1) <= 0)
    return -1;  // Address subtraction is used, so the element type must be specified
  else
    return 0;
}

// Find the address of the first element greater than the element being searched for
int upper(const void *p1, const void *p2) {
  int *a = (int *)p1;
  int *b = (int *)p2;
  if ((b == A || compare(a, b - 1) >= 0) && compare(a, b) >= 0)
    return 1;
  else if (b != A && compare(a, b - 1) < 0)
    return -1;  // Address subtraction is used, so the element type must be specified
  else
    return 0;
}

Few OI contestants now write pure C, and this method has limited usefulness, so it is not a focus here. Beginners are advised to simply use the C++ lower_bound and upper_bound functions.

Binary Search on the Answer

When solving a problem, one often considers enumerating an answer and then checking whether the enumerated value is correct. If the condition is monotonic, the requirements for binary search are met. Replacing this enumeration with binary search gives “binary search on the answer”.

Luogu P1873 Cutting Trees

Lumberjack Mirko needs to cut down MM meters of wood. This is an easy job for Mirko, because he has a beautiful new woodcutter that can cut down a forest like wildfire. However, Mirko is only allowed to cut down a single row of trees.

Mirko's woodcutter works as follows: Mirko sets a height parameter HH (in meters), and the woodcutter raises a huge saw blade to height HH and cuts off all parts of the trees above HH (of course, parts of trees no higher than HH meters remain unchanged). Mirko obtains the parts cut off from the trees.

For example, if the heights of a row of trees are 20, 15, 10, 1720,~15,~10,~17, respectively, Mirko raises the saw blade to a height of 1515 meters. After cutting, the tree heights are 15, 15, 10, 1515,~15,~10,~15, and from the 11st tree Mirko obtains 55 meters of wood, while from the 44th tree he obtains 22 meters, for a total of 77 meters.

Mirko cares deeply about environmental protection, so he will not cut down too much wood. That is why he sets the saw blade as high as possible. Your task is to help Mirko find the largest integer height HH for the saw blade such that he can obtain at least MM meters of wood. That is, if the blade is raised by another 11 meter, he will obtain less than MM meters of wood.

Solution

We could enumerate the answer from 11 to 10910^9, but this naive approach cannot obtain full marks because enumerating from 11 to 10910^9 takes too long. We can instead binary-search the answer over the interval [1, 109][1,~10^9] and check the feasibility of each candidate (usually with a greedy method). This is binary search on the answer.

Reference code
int a[1000005];
int n, m;

bool check(int k) {  // Check feasibility; k is the saw-blade height
  long long sum = 0;
  for (int i = 1; i <= n; i++)       // Check every tree
    if (a[i] > k)                    // If the tree is taller than the blade
      sum += (long long)(a[i] - k);  // Accumulate the amount of wood
  return sum >= m;                   // Meeting the minimum length means it is feasible
}

int find() {
  int l = 1, r = 1e9 + 1;   // The interval is left-closed and right-open, so add 1 to 10^9
  while (l + 1 < r) {       // While the two points are not adjacent
    int mid = (l + r) / 2;  // Take the midpoint
    if (check(mid))         // If feasible
      l = mid;              // Raise the blade height
    else
      r = mid;  // Otherwise lower the blade height
  }
  return l;  // Return the left value
}

int main() {
  cin >> n >> m;
  for (int i = 1; i <= n; i++) cin >> a[i];
  cout << find();
  return 0;
}

After reading the code above, you surely have two questions:

  1. Why is the search interval left-closed and right-open?

    At the end of the search, it looks like this (taking the valid maximum as an example):

    Figure: binary final 1

    Then it becomes

    Figure: binary final 2

    The valid minimum is exactly the opposite.

  2. Why return the left value?

    For the same reason.

Introduction

Binary search can be used to approximately find a zero of a function. If the extremum of a unimodal function is needed, ternary search is usually used.

For a function f(x)f(x), if there exists an x∗x^* such that f(x)f(x) is monotonically increasing when x<x∗x<x^* and f(x)f(x) is monotonically decreasing when x>x∗x>x^*, then f(x)f(x) is called a unimodal function. Clearly, x∗x^* is its maximizer, and f(x∗)f(x^*) is its maximum.

Why not find the extremum by finding the zero of the derivative?

In principle, after finding the derivative, it is feasible to find the zero of the derivative by binary search (because the function is unimodal, its derivative has only one zero in the same range) and thereby obtain the extremum of the unimodal function.

However, first, the process and result of differentiation can be relatively complicated for some functions.

Second, in some problems, the unimodal function whose extremum is needed is not a single function, but a function obtained by applying special operations to multiple functions (for example, finding the maximum of the minimum of several linear functions with different monotonicity). In this case, the derivative may be piecewise, and the function may not be differentiable at some points.

Note

Ternary search can find both the maximum of a unimodal function and the minimum of a single-valley function. For convenience, unless otherwise specified, the following discussion uses finding the maximum of a unimodal function as an example.

Process

The basic idea of ternary search is similar to that of binary search, but each operation chooses two points within the current interval [l,r][l,r], with lmid<rmidlmid < rmid (between the two orange points in the figure below; the two blue points in the figure). As shown below, if f(lmid)<f(rmid)f(lmid)<f(rmid), then the function must be monotonically increasing on [l,lmid)[l,lmid) (the red part below), so the maximizer (the green point below) cannot lie in this interval and it can be discarded. However, we cannot rule out the possibility that the maximizer is to the right of rmidrmid, so no more of the interval can be discarded. The converse is analogous.

Figure: ternary

The correctness of ternary search does not depend on the choice of lmidlmid and rmidrmid; usually, the two trisection points can be used. However, their choice does affect the efficiency of ternary search. Each operation discards one of the two side intervals. To reduce the number of operations, the two side intervals should be as large as possible. Therefore, choosing lmidlmid and rmidrmid as mid−εmid-\varepsilon and mid+εmid+\varepsilon, respectively, at each operation is a good choice. In fact, choosing mid±εmid\pm \varepsilon amounts to calculating, at midmid, the approximate derivative f(mid+ε)−f(mid−ε)2ε\dfrac{f(mid+\varepsilon)-f(mid-\varepsilon)}{2\varepsilon} and determining on which side of midmid the extremum lies from its sign.

Implementation

The pseudocode is as follows:

AlgorithmTernarySearch⁡(f,l,r):Input. A unimodal function f(x) and its domain [l,r].Output. The maximizer x∗, up to an error of ε, and its value f(x∗).Method. 1while r−l>ε2mid←(l+r)/23lmid←mid−ε/34rmid←mid+ε/35if f(lmid)<f(rmid)6l←lmid7else 8r←rmid9x∗←(l+r)/210return x∗, f(x∗)\begin{array}{l} \textbf{Algorithm}\operatorname{TernarySearch}(f,l,r):\\ \textbf{Input. } \text{A unimodal function } f(x) \text{ and its domain } [l,r]. \\ \textbf{Output. } \text{The maximizer }x^*\text{, up to an error of }\varepsilon\text{, and its value } f(x^*). \\ \textbf{Method. } \\ \begin{array}{ll} 1 & \textbf{while } r - l > \varepsilon\\ 2 & \qquad mid\gets (l+r)/2\\ 3 & \qquad lmid\gets mid - \varepsilon / 3 \\ 4 & \qquad rmid\gets mid + \varepsilon / 3 \\ 5 & \qquad \textbf{if } f(lmid) < f(rmid) \\ 6 & \qquad \qquad l\gets lmid \\ 7 & \qquad \textbf{else } \\ 8 & \qquad \qquad r\gets rmid \\ 9 & x^* \gets (l+r)/2 \\ 10& \textbf{return } x^*,~ f(x^*) \end{array} \end{array}
Choosing the division points

In the code, the division points are chosen as mid±ε/3mid \pm \varepsilon / 3 to guarantee that they always lie between the current ll and rr, thereby avoiding an infinite loop.

Integer case

If the domain of f(x)f(x) consists of integers, both the ternary search above and the golden-section search below should terminate when r−lr-l is small. When r−lr-l is small, the maximizer should be found by brute-force enumeration.

If a single call to f(x)f(x) is expensive, the constant factor of ternary search can be improved further with golden-section search, reducing the number of calls to f(x)f(x). This is also an important part of the optimization method proposed by Hua Luogeng.

In ternary search, each iteration requires two function calls, and after one iteration the interval length is reduced to at most 1/21/2 of its original length. This means that reaching precision ε\varepsilon requires at least

2log⁡2r−lε2\log_2\dfrac{r-l}{\varepsilon}

function calls. This is the best result ternary search can achieve. If other division points are chosen, such as trisection points, the number of calls increases because the interval shrinks more slowly after each iteration.

The idea behind golden-section search is to reuse division points calculated earlier. Thus, apart from the first iteration, which requires two function calls, all subsequent iterations require only one function call. Let the golden ratio for division be

ϕ=5−12≈0.618.\phi = \dfrac{\sqrt{5}-1}{2} \approx 0.618.

At each iteration, choose the two golden-section points:

ml=ϕl+(1−ϕ)r, mr=(1−ϕ)l+ϕr.m^l = \phi l +(1-\phi)r,~m^r = (1-\phi)l+\phi r.

The points dividing a segment in the golden ratio have a self-similar structure. In other words, mlm^l is the left golden-section point of the segment [l,r][l,r] and also the right golden-section point of the segment [l,mr][l,m^r]. The advantage of choosing the points this way is that among the points selected in iteration k>1k>1, one is guaranteed to have already been evaluated and its result can be reused directly.

Figure: golden section search

With these division points, reaching precision ε\varepsilon requires only

1+log⁡ϕ−1r−lε≈1+1.44log⁡2r−lε1 + \log_{\phi^{-1}}\dfrac{r-l}{\varepsilon} \approx 1 + 1.44\log_2\dfrac{r-l}{\varepsilon}

function calls. In the asymptotic sense, fewer function calls are needed.

The pseudocode is as follows:

AlgorithmGoldenSectionSearch⁡(f,l,r):Input. A unimodal function f(x) and its domain [l,r].Output. The maximizer x∗, up to an error of ε, and its value f(x∗).Method. 1lmid←ϕl+(1−ϕ)r2rmid←(1−ϕ)l+ϕr3lval←f(lmid)4rval←f(rmid)5while r−l>ε6if lval>rval7r←rmid8rmid←lmid9rval←lval10lmid←ϕl+(1−ϕ)r11lval←f(lmid)12else13l←lmid14lmid←rmid15lval←rval16rmid←(1−ϕ)l+ϕr17rval←f(rmid)18x∗←(l+r)/219return x∗, f(x∗)\begin{array}{l} \textbf{Algorithm}\operatorname{GoldenSectionSearch}(f,l,r):\\ \textbf{Input. } \text{A unimodal function } f(x) \text{ and its domain } [l,r]. \\ \textbf{Output. } \text{The maximizer }x^*\text{, up to an error of }\varepsilon\text{, and its value } f(x^*). \\ \textbf{Method. } \\ \begin{array}{ll} 1 & lmid \gets \phi l + (1-\phi)r \\ 2 & rmid \gets (1-\phi)l + \phi r \\ 3 & lval \gets f(lmid) \\ 4 & rval \gets f(rmid) \\ 5 & \textbf{while } r - l > \varepsilon \\ 6 & \qquad \textbf{if } lval > rval \\ 7 & \qquad \qquad r \gets rmid \\ 8 & \qquad \qquad rmid \gets lmid \\ 9 & \qquad \qquad rval \gets lval \\ 10& \qquad \qquad lmid \gets \phi l + (1-\phi)r \\ 11& \qquad \qquad lval \gets f(lmid) \\ 12& \qquad \textbf{else} \\ 13& \qquad \qquad l \gets lmid \\ 14& \qquad \qquad lmid \gets rmid \\ 15& \qquad \qquad lval \gets rval \\ 16& \qquad \qquad rmid \gets (1-\phi)l + \phi r \\ 17& \qquad \qquad rval \gets f(rmid) \\ 18& x^* \gets (l+r)/2 \\ 19& \textbf{return }x^*,~f(x^*) \end{array} \end{array}

Example Problems

Luogu P3382 - Ternary Search

Given an NN-degree polynomial and a range [l,r][l, r], find the unique value such that the function is monotonically increasing on [l,x][l, x] and monotonically decreasing on [x,r][x, r]; this value is xx.

Solution

This problem asks for the value of the independent variable when the NN-degree polynomial reaches its maximum on [l,r][l, r], so ternary search can clearly be used.

Reference code

C++

#include <cmath>
#include <iomanip>
#include <iostream>
using namespace std;

constexpr double eps = 1e-7;
int N;
double l, r, A[20], mid, lmid, rmid;

double f(double x) {
  double res = (double)0;
  for (int i = N; i >= 0; i--) res += A[i] * pow(x, i);
  return res;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  cin >> N >> l >> r;
  for (int i = N; i >= 0; i--) cin >> A[i];
  while (r - l > eps) {
    mid = (l + r) / 2;
    lmid = mid - eps;
    rmid = mid + eps;
    if (f(lmid) > f(rmid))
      r = mid;
    else
      l = mid;
  }
  cout << fixed << setprecision(6) << l;
  return 0;
}

Python

eps = 1e-6
n, l, r = map(float, input().split())
a = tuple(map(float, input().split()))[::-1]


def f(x):
    return sum(x**i * j for i, j in enumerate(a))


while r - l > eps:
    mid = (l + r) / 2
    if f(mid - eps) > f(mid + eps):
        r = mid
    else:
        l = mid
print(round(l, 6))

Exercises

Fractional Programming

See: Fractional programming

Fractional programming usually refers to the following problem: each item has two attributes cic_i and did_i, and we must select several items in some way so that ∑ci∑di\frac{\sum{c_i}}{\sum{d_i}} is maximized or minimized.

Classic examples include the maximum-ratio cycle and the maximum-ratio spanning tree.

Fractional programming can be solved with binary search.

References

This content comes from OI Wiki: Binary Search. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.