ADS 2026 · KBTU · Lecture 3 · Lab 3

Binary Search

Halve the range at every step. First the exact search in a sorted array, then the nearest element on each side of x, and finally binary search on the answer — over the range of possible answers instead of over an array.

Length ~110 minBeats 6Live demos 3Practice Lab 3 · 11 problems
01

Classic Binary Search — Exact Match

35 min board code demo: range

What to say

  • Problem: a sorted array a[0..n−1] in non-decreasing order; find an index i with a[i] == x.
  • Keep two pointers l and r — the range still in play. Look at the middle: equal — done; smaller than x — the answer is to the right; larger — to the left.
  • Every step halves the range, so the loop runs at most log₂n times — O(log n). Run the demo on n = 10⁹: thirty steps.
  • Walk through x = 13 on the board with the trace table, then write the code.
  • Then the practice problem: n numbers and k queries, answer YES/NO for each — the same function called k times.

The problem and the algorithm

Problem: we have a sorted array a[0..n-1] in non-decreasing order. We want to find an index i such that a[i] == x.

Example: find x = 13 in [3, 5, 8, 13, 18, 19, 21, 27, 32]

Algorithm

  1. Keep two pointers l and r representing the current search range
  2. While l <= r:
    • Compute mid = (l + r) / 2
    • If a[mid] == x, return mid
    • If a[mid] < x, search the right half (l = mid + 1)
    • If a[mid] > x, search the left half (r = mid - 1)

Time complexity: at each step we halve the search space → O(log n).

Trace · x = 13

index:   0  1  2  3  4  5  6  7  8
a:       3  5  8 13 18 19 21 27 32
step    l    r  mid  a[mid]   
------------------------------------------------------
   1    0    8    4      18   18 > 13 → r = 3
   2    0    3    1       5   5 < 13 → l = 2
   3    2    3    2       8   8 < 13 → l = 3
   4    3    3    3      13   a[mid] == 13 → found at 3

4 steps, and the range shrinks 9 → 4 → 2 → 1. For a million elements binary search needs at most 20 steps; a linear scan needs up to a million.

Demo · watch the range collapse

Binary, comparisons
—
Linear, comparisons
—
steplmrrange

Implementation

#include <iostream>
#include <vector>
using namespace std;

int binary_search_exact(vector<int>& a, int x) {
    int l = 0, r = (int)a.size() - 1;
    while (l <= r) {
        int mid = l + (r - l) / 2;  // Avoids overflow
        if (a[mid] == x) return mid;
        if (a[mid] < x) l = mid + 1;
        else r = mid - 1;
    }
    return -1; // not found
}

int main() {
    vector<int> a = {3, 5, 8, 13, 18, 19, 21, 27, 32};
    int x = 13;
    int idx = binary_search_exact(a, x);
    cout << (idx != -1 ? "Found at index " + to_string(idx) : "Not found") << "\n";
    return 0;
}
output
Found at index 3

l + (r - l) / 2 is the same middle as (l + r) / 2, but l + r can overflow int when both are close to 2·10⁹.

Another version · returning the index

The same search with (l + r) / 2 and an else if chain. Returns the index, or −1.

the full program25 lines
#include <iostream>
#include <vector>

using namespace std;

int binary_search(vector<int> &a, int x) {
    int l = 0, r = a.size() - 1;
    while (l <= r) {
        int mid = (l + r) / 2;
        if (a[mid] == x)
            return mid;
        else if (a[mid] > x)
            r = mid - 1;
        else
            l = mid + 1;
    }
    return -1;
}

int main() {
    vector<int> a = {3, 5, 8, 13, 18, 19, 21, 27, 32};
    int ind = binary_search(a, 13);
    cout << ind;
    return 0;
}
output
3

Another version · printing l and r

The same function with one extra line — cout << l << " " << r at the top of the loop — so the range can be watched shrinking. It reads the array and x from input.

the full program36 lines
#include <iostream>
#include <vector>

using namespace std;

int binary_search(vector<int> &a, int x) {
    int l = 0, r = a.size() - 1;
    while (l <= r) {
        cout << l << " " << r << endl;
        int m = (l + r) / 2;
        if (a[m] == x)
            return m;
        if (a[m] > x)
            r = m - 1;
        else
            l = m + 1;
    }
    return -1;
}

int main() {
    int n;
    vector<int> a;
    cin >> n;
    for (int i = 0; i < n; i++) {
        int t;
        cin >> t;
        a.push_back(t);
    }
    int x;
    cin >> x;
    int ind = binary_search(a, x);
    cout << ind;
        
    return 0;
}
input
9
3 5 8 13 18 19 21 27 32
13
output
0 8
0 3
2 3
3 3
3

Practice problem · does x occur in the array?

Given an array and k queries, check whether each queried element exists. Codeforces EDU · Binary Search · A

#include <iostream>
#include <vector>
using namespace std;

bool find(vector<int> &a, int x) {
    int l = 0;
    int r = a.size() - 1;
    while (l <= r) {
        int m = (l + r) / 2;
        if (a[m] == x)
            return true;
        if (a[m] < x) 
            l = m + 1;
        else
            r = m - 1;
    }
    return false;
}

int main() {
    int n, k;
    cin >> n >> k;

    vector<int> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];

    int x;
    for (int i = 0; i < k; i++) {
        cin >> x;
        if (find(a, x))
            cout << "YES\n";
        else
            cout << "NO\n";
    }
    return 0;
}
input
5 3
1 3 5 7 9
3 4 9
output
YES
NO
YES

Another version · exists()

The same solution with the function called exists and the array read with push_back.

the full program36 lines
#include <iostream>
#include <vector>

using namespace std;

bool exists(vector<int> &a, int x) {
    int l = 0, r = a.size() - 1;
    while (l <= r) {
        int mid = (l + r) / 2;
        if (a[mid] == x)
            return true;
        if (a[mid] > x) 
            r = mid - 1;
        else
            l = mid + 1;
    }
    return false;
}

int main() {
    int n, k, t;
    cin >> n >> k;
    vector<int> a;
    for (int i = 0; i < n; i++) {
        cin >> t;
        a.push_back(t);
    }
    for (int i = 0; i < k; i++) {
        cin >> t;
        if (exists(a, t))
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}
input
5 3
1 3 5 7 9
3 4 9
output
YES
NO
YES
02

Finding the Nearest Element

22 min board code demo: bounds

What to say

  • Now x may be absent, and we want its neighbours: the largest element ≤ x and the smallest element ≥ x.
  • Pretend there is −∞ before the array and +∞ after it: start with l = −1, r = n. The invariant is a[l] ≤ x < a[r]; the loop runs while the two are not neighbours.
  • When it stops, l is the largest element ≤ x, and r the smallest element > x. For "≥ x" run the mirrored search — keep a[l] < x and a[r] ≥ x.
  • In the demo, an array full of repeats: the two boundaries are std::lower_bound and std::upper_bound, and their difference is how many times x occurs.

Goal and algorithm

Goal: find

  • the largest element ≤ x — the closest from the left
  • the smallest element ≥ x — the closest from the right

Algorithm for the largest ≤ x

  1. Add −∞ at the start and +∞ at the end — conceptually: l = -1, r = n
  2. Maintain the invariant: a[l] ≤ x and a[r] > x
  3. While l + 1 < r:
    • mid = (l + r) / 2
    • if a[mid] ≤ x → move l = mid
    • else r = mid

Trace · x = 20

index:   0  1  2  3  4  5  6  7  8
a:       3  5 10 11 13 18 25 27 31
step    l    r  mid  a[mid]   
------------------------------------------------------
   1   -1    9    4      13   13 ≤ 20 → l = 4
   2    4    9    6      25   25 > 20 → r = 6
   3    4    6    5      18   18 ≤ 20 → l = 5

The loop stops with l = 5, r = 6: a[l] = 18 is the largest element ≤ 20, a[r] = 25 the smallest element greater than 20.

Implementation

#include <bits/stdc++.h>
using namespace std;

pair<int,int> nearest_elements(vector<int>& a, int x) {
    int n = a.size();
    int l = -1, r = n; // conceptual -∞ and +∞
    
    while (l + 1 < r) {
        int mid = (l + r) / 2;
        if (a[mid] <= x) l = mid;
        else r = mid;
    }
    
    int leftVal = (l >= 0 ? a[l] : INT_MIN);
    // a[r] is the smallest element > x; for the smallest element >= x
    // run the mirrored search: keep a[l2] < x and a[r2] >= x
    int l2 = -1, r2 = n;
    while (l2 + 1 < r2) {
        int mid = (l2 + r2) / 2;
        if (a[mid] < x) l2 = mid;
        else r2 = mid;
    }
    int rightVal = (r2 < n ? a[r2] : INT_MAX);
    return {leftVal, rightVal};
}

int main() {
    vector<int> a = {3, 5, 10, 11, 13, 18, 25, 27, 31};
    int x = 20;
    auto [leftVal, rightVal] = nearest_elements(a, x);
    cout << "Closest left: " << leftVal << ", closest right: " << rightVal << "\n";

    x = 18;
    auto [l18, r18] = nearest_elements(a, x);
    cout << "x = 18 -> left: " << l18 << ", right: " << r18 << "\n";
    return 0;
}
output
Closest left: 18, closest right: 25
x = 18 -> left: 18, right: 18

After the first loop a[r] is the smallest element strictly greater than x. When x itself is in the array that is the wrong answer for "≥ x": for x = 18 it would give 25. The second, mirrored loop keeps a[l2] < x ≤ a[r2] and gives 18.

Another version · closest to the left

Returns the index of the largest element ≤ x — l, or −1 if there is none. The program prints l + 1: the 1-based position, which is also how many elements are ≤ x.

int index_(vector<int> &a, int x) {
    int l = -1, r = a.size(); 
    while (r > l + 1) {
        int mid = (l + r) / 2;
        if (a[mid] <= x)
            l = mid;
        else
            r = mid;
        // cout << l << " " << r << " " << mid << endl;
    }
    return l;
}
the full program32 lines
#include <iostream>
#include <vector>

using namespace std;

int index_(vector<int> &a, int x) {
    int l = -1, r = a.size(); 
    while (r > l + 1) {
        int mid = (l + r) / 2;
        if (a[mid] <= x)
            l = mid;
        else
            r = mid;
        // cout << l << " " << r << " " << mid << endl;
    }
    return l;
}

int main() {
    int n, k, t;
    vector<int> a;
    cin >> n >> k;
    for (int i = 0; i < n; i++) {
        cin >> t;
        a.push_back(t);
    }
    for (int i = 0; i < k; i++) {
        cin >> t;
        cout << index_(a, t) + 1 << endl;
    }
    return 0;
}
input
5 5
1 3 5 7 9
2 4 8 1 10
output
1
2
4
1
5

Another version · closest to the right

The mirrored search: a[mid] ≥ x moves r. Returns the index of the smallest element ≥ x — r, or n if there is none.

int index_right(vector<int> &a, int x) {
    int l = -1, r = a.size(); 
    while (r > l + 1) {
        int mid = (l + r) / 2;
        if (a[mid] >= x)
            r = mid;
        else
            l = mid;
        // cout << l << " " << r << " " << mid << endl;
    }
    return r;
}
the full program32 lines
#include <iostream>
#include <vector>

using namespace std;

int index_right(vector<int> &a, int x) {
    int l = -1, r = a.size(); 
    while (r > l + 1) {
        int mid = (l + r) / 2;
        if (a[mid] >= x)
            r = mid;
        else
            l = mid;
        // cout << l << " " << r << " " << mid << endl;
    }
    return r;
}

int main() {
    int n, k, t;
    vector<int> a;
    cin >> n >> k;
    for (int i = 0; i < n; i++) {
        cin >> t;
        a.push_back(t);
    }
    for (int i = 0; i < k; i++) {
        cin >> t;
        cout << index_right(a, t) + 1 << endl;   // was index_(...), which is not defined here
    }
    return 0;
}
input
5 5
1 3 5 7 9
2 4 8 1 10
output
2
3
5
1
6

Demo · bounds on a array with repeats

lower_bound
—
upper_bound
—
count of x
—
Blue is the block equal to x.
03

Binary Search on the Answer

35 min board code demo: ropes

What to say

  • We look for the smallest (or largest) x for which a monotonic condition holds. There is no array — we search the range of possible answers.
  • Example 1. n rectangles a × b into the smallest square. f(x) = ⌊x/a⌋·⌊x/b⌋ ≥ n. If side x fits them, any larger side fits too — monotonic, so find the first x where it becomes true.
  • Upper bound for the answer: max(a, b) · n — n rectangles in one row always fit. Use long long: the bound and the product both overflow int.
  • Example 2. n ropes, k equal pieces, maximise the length. If length x works, every shorter one works too — so keep the largest x that is still true.
  • The answer is real, so do not wait for l == r: run a fixed 100 iterations — or loop while r − l > eps.

The idea

We are looking for the smallest (or largest) x such that some monotonic condition holds. Instead of searching an array, we search in the range of possible answers.

Example 1 · minimum square to fit rectangles

Problem: we have n rectangles of size a × b. Find the smallest side of a square that can fit all of them (no rotation).

Monotonic function: f(x) = (⌊x / a⌋ × ⌊x / b⌋ ≥ n). If a square of side x can fit them, a larger square can too → monotonic.

#include <iostream>
using namespace std;

bool canFit(long long side, long long a, long long b, long long n) {
    return (side / a) * (side / b) >= n;
}

long long minSquareSide(long long a, long long b, long long n) {
    long long l = 0, r = max(a, b) * n; // upper bound
    
    while (l + 1 < r) {
        long long mid = (l + r) / 2;
        if (canFit(mid, a, b, n)) r = mid;
        else l = mid;
    }
    return r;
}

int main() {
    long long n = 10, a = 2, b = 3;
    cout << "Minimum side: " << minSquareSide(a, b, n) << "\n";
    return 0;
}
output
Minimum side: 9

Check: side 9 holds 4 × 3 = 12 rectangles 2 × 3 — enough for 10; side 8 holds only 8.

Another version · long long throughout

The same solution reading w, h and n from input.

the full program28 lines
#include <iostream>
#define ll long long

using namespace std;

ll can_fit(ll n, ll w, ll h) {
    return (n / w) * (n / h);
}

ll solve(ll w, ll h, ll n) {
    ll l = 0; ll r = max(w, h) * n;
    while (l + 1 < r) {
        ll mid = (l + r) / 2;
        if (can_fit(mid, w, h) >= n)
            r = mid;
        else
            l = mid;
    }
    return r;
}


int main() {
    ll w, h, n;
    cin >> w >> h >> n;
    cout << solve(w, h, n);
    return 0;
}
input
2 3 10
output
9

Another version · with int and a trace

The first draft: prints l r mid at every step. It is written with int, which is enough for small inputs only — max(n, m) * k overflows when the numbers are large.

the full program28 lines
#include <iostream>

using namespace std;
int n, m, k;

bool f(int x) {
    return (x / n) * (x / m) >= k;
}

int solve(int n, int m, int k) {
    int l = 0;
    int r = max(n, m) * k;   // overflows int for large n, m, k — use long long
    while (l + 1 < r) {
        cout << l << " " << r << " " << (l + r) / 2 << endl;
        int x = (l + r) / 2;
        if (f(x) == true)
            r = x;
        else
            l = x;
    }
    return r;
}

int main() {
    cin >> n >> m >> k;
    cout << solve(n, m, k);
    return 0;
}

Example 2 · maximum rope length

Problem: we have n ropes and want k equal pieces of the maximum length.

Monotonic function: f(x) = can we cut ≥ k pieces of length x? If we can cut pieces of length x, we can cut shorter ones too → monotonic decreasing, so the search keeps the largest x that is still true.

#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;

bool canCut(const vector<double>& ropes, double len, int k) {
    long long count = 0;
    for (double r : ropes) 
        count += (long long)(r / len);
    return count >= k;
}

double maxRopeLength(vector<double>& ropes, int k) {
    double l = 0, r = *max_element(ropes.begin(), ropes.end());
    
    for (int iter = 0; iter < 100; iter++) { // enough iterations for precision
        double mid = (l + r) / 2;
        if (canCut(ropes, mid, k)) l = mid;
        else r = mid;
    }
    return l;
}

int main() {
    int n, k;
    cin >> n >> k;
    
    vector<double> ropes(n);
    for (int i = 0; i < n; i++) {
        cin >> ropes[i];
    }
    
    cout << fixed << setprecision(2) << "Max length: " << maxRopeLength(ropes, k) << "\n";
    return 0;
}

// Example input:
// 4 11
// 802
// 743
// 457
// 539
input
4 11
802
743
457
539
output
Max length: 200.50

Another version · stop by precision

Instead of 100 iterations the loop runs while r − l > eps with eps = 1e-8, starting from r = 1e9.

the full program38 lines
#include <iostream>
#include <vector>

using namespace std;

double eps = 1e-8;

bool ok(vector<double> &a, int n, double t) {
    int p = 0;
    for (int i = 0; i < a.size(); i++)
        p += (int) (a[i] / t);
    return p >= n;
}

double solve(vector<double> &a, int n) {
    double l = 0, r = 1e9;
    while (r - l > eps) {
        double mid = (l + r) / 2.0;
        if (!ok(a, n, mid)) 
            r = mid;
        else
            l = mid;
    }
    return l;
}

int main() {
    int n, k;
    cin >> n >> k;
    vector<double> a;
    for (int i = 0; i < n; i++) {
        double q;
        cin >> q;
        a.push_back(q);
    }
    cout << solve(a, k);
    return 0;
}
input
4 11
802
743
457
539
output
200.5

Another version · cube root

A real-valued answer with nothing to cut: the cube root of x. f(y) = y³ grows, so find the point where it reaches x, with precision eps = 1e-3. The loop prints the range and f(mid) at every step.

the full program29 lines
#include <iostream>

using namespace std;
double x;
double eps = 1e-3;

double f(double y) {
    return y * y * y;
}

double solve(double x) {
    double l = 0, r = max(1.0, x);   // for x < 1 the cube root is bigger than x
    while (r - l > eps) {
        cout << l << " " << r << " " << (l + r) / 2.0 << endl;
        double y = (l + r) / 2;
        cout << f(y) << endl;
        if (f(y) >= x)
            r = y;
        else
            l = y;
    }
    return l;
}

int main() {
    cin >> x;
    cout << solve(x);
    return 0;
}

The upper bound must contain the answer: for x < 1 the cube root is bigger than x (∛0.125 = 0.5), so r = x is not enough — r = max(1.0, x) is.

Demo · cutting the ropes (problem G)

Longest piece
—
Iterations
—
If we tried every length
—
lmrpieces at mcan(m)

The can column only ever goes from yes to no as you read down a fixed column — that is monotonicity, made visible.

04

Key Takeaways

4 min

Five things to remember

  1. Binary search works on sorted data or monotonic conditions
  2. Search the answer space: sometimes we search not an array but a range of possible answers
  3. Floating point precision: when the answer is a floating point number, iterate a fixed number of times (e.g. 100) to avoid infinite loops
  4. Always define a clear monotonic function f(x) before implementing binary search on the answer
  5. Time complexity: O(log n) for array search, O(log(max − min) × cost of f) for answer search
05

Binary Search Variants

6 min board

Four variants

TypeUse caseTemplate
Exact Matchfind a specific elementwhile (l <= r)
Lower Boundfirst element ≥ xwhile (l < r)
Upper Boundfirst element > xwhile (l < r)
Answer Searchoptimise on a monotonic functionwhile (l < r) with a condition

The code on this page writes lower bound, upper bound and answer search in the other common style — l = -1, r = n, while (l + 1 < r), with a[l] and a[r] always on opposite sides of the boundary. Both styles are correct; pick one and keep its invariant.

06

Lab 3 — the problems

8 min ADS-Lab-03

Problem to topic

A
Binary searchnew
Exact match — the plain search, n ≤ 100
B
Patchwork Staccato I
Nearest element inside a segment; n, q ≤ 100, so brute force still passes
C
Debugging
Search on the answer — how many blocks until M mistakes are found
D
Win me if you can!
Sort once, then an upper bound plus a prefix sum per round
E
Patchwork Staccato II
The same statement as B with n, q ≤ 10⁵ — brute force now dies
F
Robin Hood stealing the Gold
Search on the answer — minimum bars per hour, ceilings not floors
G
Cutting the Ropesnew
Search on the answer — the longest equal piece, Σ⌊aᵢ/x⌋ ≥ k
H
K-subarray
Prefix sums with a set — the one problem that is not binary search
I
Oshiete oshiete yo sono shikumi wo
Search on the answer over a split of the street
J
Jonathan the Farmer
Place K items as far apart as possible — search on the distance, TL 5 s
K
Snake
Search a virtual array — the snake is never built
B and E are the same problem twice. B has n, q ≤ 100, so a double loop passes; E raises both to 10⁵ and the double loop dies. Hand them out as a pair.
J has TL 5 s — n = 2·10⁵ with a log inside a log. Suggested order: A → D → F → G → C → J.