This page briefly introduces binary search, ternary search derived from binary search, and binary search on the answer.
Binary Search
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 .
The average-case and worst-case time complexities of binary search are both . During the search, the algorithm halves the query interval each time, so an array of length requires at most searches.
Space Complexity
The iterative version of binary search has space complexity .
The recursive version of binary search (without tail-call elimination) has space complexity .
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 is a signed number and you can guarantee that , 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 and not satisfying it by , 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:
- The answer lies in a fixed interval.
- 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.
- The feasible solutions have a certain monotonicity over the interval. In other words, if satisfies the condition, then either or 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 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 (in meters), and the woodcutter raises a huge saw blade to height and cuts off all parts of the trees above (of course, parts of trees no higher than meters remain unchanged). Mirko obtains the parts cut off from the trees.
For example, if the heights of a row of trees are , respectively, Mirko raises the saw blade to a height of meters. After cutting, the tree heights are , and from the st tree Mirko obtains meters of wood, while from the th tree he obtains meters, for a total of 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 for the saw blade such that he can obtain at least meters of wood. That is, if the blade is raised by another meter, he will obtain less than meters of wood.
Solution
We could enumerate the answer from to , but this naive approach cannot obtain full marks because enumerating from to takes too long. We can instead binary-search the answer over the interval 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:
Ternary Search
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 , if there exists an such that is monotonically increasing when and is monotonically decreasing when , then is called a unimodal function. Clearly, is its maximizer, and 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 , with (between the two orange points in the figure below; the two blue points in the figure). As shown below, if , then the function must be monotonically increasing on (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 , so no more of the interval can be discarded. The converse is analogous.
The correctness of ternary search does not depend on the choice of and ; 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 and as and , respectively, at each operation is a good choice. In fact, choosing amounts to calculating, at , the approximate derivative and determining on which side of the extremum lies from its sign.
Implementation
The pseudocode is as follows:
Choosing the division points
In the code, the division points are chosen as to guarantee that they always lie between the current and , thereby avoiding an infinite loop.
Integer case
If the domain of consists of integers, both the ternary search above and the golden-section search below should terminate when is small. When is small, the maximizer should be found by brute-force enumeration.
Optimization: Golden-Section Search
If a single call to is expensive, the constant factor of ternary search can be improved further with golden-section search, reducing the number of calls to . 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 of its original length. This means that reaching precision requires at least
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
At each iteration, choose the two golden-section points:
The points dividing a segment in the golden ratio have a self-similar structure. In other words, is the left golden-section point of the segment and also the right golden-section point of the segment . The advantage of choosing the points this way is that among the points selected in iteration , one is guaranteed to have already been evaluated and its result can be reused directly.
With these division points, reaching precision requires only
function calls. In the asymptotic sense, fewer function calls are needed.
The pseudocode is as follows:
Example Problems
Luogu P3382 - Ternary Search
Given an -degree polynomial and a range , find the unique value such that the function is monotonically increasing on and monotonically decreasing on ; this value is .
Solution
This problem asks for the value of the independent variable when the -degree polynomial reaches its maximum on , 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
- UVa 1476 - Error Curves
- UVa 10385 - Duathlon
- UOJ 162 -【Tsinghua Training Camp 2015】Light Bulb Test
- Luogu P7579 - “RdOI R2” Weighing (weigh)
Fractional Programming
Fractional programming usually refers to the following problem: each item has two attributes and , and we must select several items in some way so that 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.