← back

Binary Search

Binary Search on Answer Space

When the answer is monotone (if x works, all larger/smaller x also work), binary search over the possible answer range with a feasibility check function. Used for minimize maximum, maximize minimum, koko eating, ship packages.

Suppose you have an array of positive integers and you need to split it into k contiguous subarrays so that the largest subarray sum is as small as possible. This is LeetCode 410, Split Array Largest Sum, and it is one of those problems that looks like it should be solved with dynamic programming. You could define dp[i][j] as the minimum largest-sum when splitting the first i elements into j groups, and you would be right: that works. But it runs in O(n2⋅k)O(n^2 \cdot k) time and the code is fiddly. There is a far more elegant approach hiding in plain sight, and once you see it, you will start recognizing it everywhere.

The trick is to stop searching for the best split and instead binary search on the answer itself.

The insight: search on the answer

Here is the mental shift. Instead of asking "what is the optimal way to split this array?", ask a different question: "if I told you that no subarray is allowed to exceed a capacity of X, could you split the array into k or fewer parts?"

That question is dramatically easier to answer. You just walk left to right, greedily packing elements into the current group. When adding the next element would push the group sum past X, you start a new group. If you finish with k or fewer groups, then X is feasible. If you need more than k groups, X is too small.

The beautiful part is that feasibility is monotonic. If a capacity of X works, then X + 1 also works, since a larger budget can only make things easier. And if X is too small, then X - 1 is also too small. This monotonicity is exactly what binary search requires. There is a sharp boundary between "too small" and "feasible," and binary search finds it in O(log⁡(range))O(\log(\text{range})) probes.

The feasibility check

The feasibility function is the heart of this pattern. For Split Array Largest Sum, it looks like this: walk through the array, maintaining a running sum for the current group. When adding the next element would exceed the capacity, close the current group and start a new one. Count how many groups you need. If the count is within budget, return True.

1
2
3
4
5
6
7
8
9
def feasible(nums, cap, k):
    groups = 1
    current_sum = 0
    for x in nums:
        if current_sum + x > cap:
            groups += 1
            current_sum = 0
        current_sum += x
    return groups <= k

This runs in O(n)O(n): a single pass through the array. No recursion, no memoization, no complicated state.

Walking through an example

Take nums = [7, 2, 5, 10, 8] with k = 2. We need to split this into at most 2 contiguous subarrays, minimizing the maximum subarray sum.

First, what are the bounds for our binary search? The smallest possible answer is max(nums) = 10, because every element must fit in some group. The largest possible answer is sum(nums) = 32, which corresponds to putting everything in one group. So lo = 10 and hi = 32.

Iteration 1: mid = (10 + 32) // 2 = 21. Is capacity 21 feasible? Walk through: 7 (sum=7), then 7+2=9, then 9+5=14, then 14+10=24 > 21, so start a new group. Groups so far: 1 completed, now in group 2. Continue: 10 (sum=10), then 10+8=18. Done with 2 groups. 2 <= 2, so feasible. Set hi = 21.

Iteration 2: mid = (10 + 21) // 2 = 15. Is 15 feasible? Walk through: 7, 7+2=9, 9+5=14, 14+10=24 > 15, new group. 10, 10+8=18 > 15, new group. 8. That is 3 groups, but we only have budget for 2. Not feasible. Set lo = 16.

Iteration 3: mid = (16 + 21) // 2 = 18. Is 18 feasible? Walk through: 7, 9, 14, 24 > 18, new group. 10, 18. Two groups total. Feasible. Set hi = 18.

Iteration 4: mid = (16 + 18) // 2 = 17. Is 17 feasible? Walk through: 7, 9, 14, 24 > 17, new group. 10, 18 > 17, new group. 8. Three groups. Not feasible. Set lo = 18.

Now lo == hi == 18. The answer is 18, corresponding to the split [7, 2, 5] | [10, 8] with subarray sums 14 and 18.

The complete solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def splitArray(nums, k):
    def feasible(cap):
        groups = 1
        current_sum = 0
        for x in nums:
            if current_sum + x > cap:
                groups += 1
                current_sum = 0
            current_sum += x
        return groups <= k

    lo, hi = max(nums), sum(nums)
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

Koko Eating Bananas

The problem LeetCode 875 dresses the same idea in a different story. Koko has piles of bananas and h hours to eat them all. Each hour, she picks a pile and eats up to speed bananas from it. If the pile has fewer than speed bananas, she finishes that pile and waits out the rest of the hour. What is the minimum speed that lets her finish in time?

The answer space is clear: the minimum speed is 1 (eat one banana per hour), and the maximum useful speed is max(piles) (she can finish any pile in one hour). Monotonicity holds: if speed s is fast enough, then s + 1 is also fast enough.

The feasibility check changes: for a given speed, calculate how many hours each pile takes (ceiling division: (pile + speed - 1) // speed), sum those hours, and check whether the total is within h.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def minEatingSpeed(piles, h):
    def feasible(speed):
        hours = 0
        for pile in piles:
            hours += (pile + speed - 1) // speed
        return hours <= h

    lo, hi = 1, max(piles)
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

The structure is identical. Only the feasibility check is different.

Capacity to Ship Packages Within D Days

Consider LeetCode 1011, yet another instance. You have packages with given weights that must be shipped in order over d days. What is the minimum ship capacity? The answer range is [max(weights), sum(weights)]. The feasibility check is essentially the same as Split Array Largest Sum: greedily load packages onto the current day's ship until the next package would exceed capacity, then start a new day.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def shipWithinDays(weights, days):
    def feasible(cap):
        day_count = 1
        current_load = 0
        for w in weights:
            if current_load + w > cap:
                day_count += 1
                current_load = 0
            current_load += w
        return day_count <= days

    lo, hi = max(weights), sum(weights)
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

If this looks almost identical to Split Array Largest Sum, that is because it is. The outer binary search is exactly the same. The inner check is exactly the same. They are the same problem wearing different hats.

The general pattern

Every binary-search-on-answer problem has the same skeleton:

  1. Identify the answer range. Find the smallest possible answer (lo) and the largest possible answer (hi). For "minimize the maximum" problems, lo is usually the maximum single element and hi is the total sum.
  1. Write a feasible(mid) function. This is where the real work is. It takes a candidate answer and returns True if that answer is achievable. The feasibility check is typically a greedy O(n)O(n) scan.
  1. Binary search. Use the lo < hi template with hi = mid when feasible and lo = mid + 1 when not. Return lo.

Why lo < hi with hi = mid

This is a subtle but important detail. When feasible(mid) returns True, you know mid is a valid answer, but there might be a smaller valid answer. So you set hi = mid, keeping mid in the search space. When it returns False, mid is definitely not the answer, so you can safely exclude it with lo = mid + 1.

The loop terminates when lo == hi, at which point they both point to the smallest feasible value. If you instead used hi = mid - 1, you might skip over the answer: mid could be the exact boundary, and shrinking past it loses it forever.

This is one of the most common sources of binary search bugs. When in doubt, use lo < hi with hi = mid for "find the minimum feasible" problems, and lo < hi with lo = mid + 1 and hi = mid (never mid - 1) as your default.

Common mistakes

Wrong bounds. If you set lo too high, you might miss the answer. If you set hi too low, same problem. For Split Array Largest Sum, setting lo = 0 would still produce the correct answer (just slower), but setting lo = min(nums) instead of max(nums) means your feasibility check might encounter an element larger than the capacity, which the greedy algorithm does not handle well.

Feasibility logic errors. The greedy check must match the problem constraints exactly. In Split Array Largest Sum, you start with groups = 1 (not 0) because you are already in the first group. Off-by-one errors here silently shift your answer by one.

Forgetting to handle single elements. If any single element exceeds the capacity, the problem is infeasible. Setting lo = max(nums) ensures this never happens: you never test a capacity smaller than the largest element.

Minimize Max Distance to Gas Station

With LeetCode 774 comes a continuous twist. You have gas stations at given positions along a highway, and you can add k new stations anywhere. You want to minimize the maximum distance between adjacent stations.

The answer space is continuous: lo = 0 and hi = max gap between consecutive stations. For a candidate maximum gap d, the feasibility check counts how many new stations you need. For each existing gap g, you need ceil(g / d) - 1 stations to break it into pieces no larger than d. If the total is within k, d is feasible.

Because the answer is a real number, use a fixed number of iterations (around 100) or loop while hi - lo > 1e-6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def minmaxGasDist(stations, k):
    def feasible(d):
        count = 0
        for i in range(len(stations) - 1):
            gap = stations[i + 1] - stations[i]
            count += int(gap / d)
        return count <= k

    lo, hi = 0, max(stations[i + 1] - stations[i]
                     for i in range(len(stations) - 1))
    for _ in range(100):
        mid = (lo + hi) / 2
        if feasible(mid):
            hi = mid
        else:
            lo = mid
    return lo

Note two differences from the integer variants. First, lo = mid instead of lo = mid + 1, because mid is a real number and we cannot "step past" it. Second, we loop a fixed number of times rather than checking lo < hi, since floating-point equality is unreliable. After 100 iterations the search range shrinks by a factor of 21002^{100}, which is far more precision than you need.

Minimum Number of Days to Make m Bouquets

Take LeetCode 1482, which searches on time. You have n flowers, each blooming on a given day. You need m bouquets, each requiring k adjacent bloomed flowers. What is the earliest day you can make all bouquets?

The answer range is [min(bloomDay), max(bloomDay)]. For a candidate day d, walk through the array counting consecutive flowers that have bloomed (i.e., bloomDay[i] <= d). Every time the streak reaches k, you have one bouquet and reset the streak. If you collect m bouquets, d is feasible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def minDays(bloomDay, m, k):
    if m * k > len(bloomDay):
        return -1

    def feasible(day):
        bouquets = 0
        streak = 0
        for b in bloomDay:
            if b <= day:
                streak += 1
                if streak == k:
                    bouquets += 1
                    streak = 0
            else:
                streak = 0
        return bouquets >= m

    lo, hi = min(bloomDay), max(bloomDay)
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

The early guard m * k > len(bloomDay) handles the impossible case: if you need more flowers than exist, no amount of waiting helps.

Complexity analysis

The binary search runs O(log⁡(hi−lo))O(\log(hi - lo)) iterations. For Split Array Largest Sum, the range is sum(nums) - max(nums), which is at most O(n⋅max_val)O(n \cdot \text{max\_val}). Each iteration runs the feasibility check in O(n)O(n). So the total time complexity is O(n⋅log⁡(sum−max))O(n \cdot \log(\text{sum} - \text{max})), which in practice is O(n⋅log⁡(n⋅max_val))O(n \cdot \log(n \cdot \text{max\_val})).

Space complexity is O(1)O(1): you only need a few variables for the binary search and the greedy scan.

Compared to the O(n2⋅k)O(n^2 \cdot k) DP approach, this is dramatically faster and far simpler to implement. That is the magic of binary search on the answer space: you replace a hard optimization problem with a sequence of easy yes/no questions.

Recognizing this pattern

You're probably looking at binary search on the answer when:

  • The problem asks to "minimize the maximum" or "maximize the minimum" of something.
  • You can phrase the question as "Can we achieve X with budget Y?", a feasibility predicate that is monotonic in X.
  • Brute-forcing the optimum is hard, but checking feasibility for a fixed candidate value is an easy greedy or simulation.
  • The answer is a number bounded by an obvious range (e.g., max element to total sum, 1 to max-coord) and you want O(log⁡(range)⋅n)O(\log(\text{range}) \cdot n).

Common templates:

Practice Problems (70)

⊣ 275 H-Index II
⊣ 410 Split Array Largest Sum
⊣ 475 Heaters
⊣ 668 Kth Smallest Number in Multiplication Table
⊣ 719 Find K-th Smallest Pair Distance
⊣ 793 Preimage Size of Factorial Zeroes Function
⊣ 875 Koko Eating Bananas
⊣ 1011 Capacity To Ship Packages Within D Days
⊣ 1283 Find the Smallest Divisor Given a Threshold
⊣ 1300 Sum of Mutated Array Closest to Target
⊣ 1482 Minimum Number of Days to Make m Bouquets
⊣ 1552 Magnetic Force Between Two Balls
⊣ 1648 Sell Diminishing-Valued Colored Balls
⊣ 1739 Building Boxes
⊣ 1760 Minimum Limit of Balls in a Bag
⊣ 1802 Maximum Value at a Given Index in a Bounded Array
⊣ 1870 Minimum Speed to Arrive on Time
⊣ 1898 Maximum Number of Removable Characters
⊣ 1901 Find a Peak Element II
⊣ 1954 Minimum Garden Perimeter to Collect Enough Apples
⊣ 2040 Kth Smallest Product of Two Sorted Arrays
⊣ 2064 Minimized Maximum of Products Distributed to Any Store
⊣ 2071 Maximum Number of Tasks You Can Assign
⊣ 2141 Maximum Running Time of N Computers
⊣ 2187 Minimum Time to Complete Trips
⊣ 2226 Maximum Candies Allocated to K Children
⊣ 2333 Minimum Sum of Squared Difference
⊣ 2358 Maximum Number of Groups Entering a Competition
⊣ 2439 Minimize Maximum of Array
⊣ 2448 Minimum Cost to Make Array Equal
⊣ 2498 Frog Jump II
⊣ 2513 Minimize the Maximum of Two Arrays
⊣ 2517 Maximum Tastiness of Candy Basket
⊣ 2528 Maximize the Minimum Powered City
⊣ 2594 Minimum Time to Repair Cars
⊣ 2601 Prime Subtraction Operation
⊣ 2790 Maximum Number of Groups With Increasing Length
⊣ 2817 Minimum Absolute Difference Between Elements With Constraint
⊣ 2861 Maximum Number of Alloys
⊣ 2967 Minimum Cost to Make Array Equalindromic
⊣ 2968 Apply Operations to Maximize Frequency Score
⊣ 2981 Find Longest Special Substring That Occurs Thrice I
⊣ 2982 Find Longest Special Substring That Occurs Thrice II
⊣ 3048 Earliest Second to Mark Indices I
⊣ 3049 Earliest Second to Mark Indices II
⊣ 3134 Find the Median of the Uniqueness Array
⊣ 3143 Maximum Points Inside the Square
⊣ 3145 Find Products of Elements of Big Array
⊣ 3281 Maximize Score of Numbers in Ranges
⊣ 3296 Minimum Number of Seconds to Make Mountain Height Zero
⊣ 3350 Adjacent Increasing Subarrays Detection II
⊣ 3356 Zero Array Transformation II
⊣ 3357 Minimize the Maximum Adjacent Element Difference
⊣ 3398 Smallest Substring With Identical Characters I
⊣ 3399 Smallest Substring With Identical Characters II
⊣ 3413 Maximum Coins From K Consecutive Bags
⊣ 3419 Minimize the Maximum Edge Weight of Graph
⊣ 3449 Maximize the Minimum Game Score
⊣ 3453 Separate Squares I
⊣ 3464 Maximize the Distance Between Points on a Square
⊣ 3477 Fruits Into Baskets II
⊣ 3479 Fruits Into Baskets III
⊣ 3605 Minimum Stability Factor of Array
⊣ 3608 Minimum Time for K Connected Components
⊣ 3613 Minimize Maximum Component Cost
⊣ 3639 Minimum Time to Activate String
⊣ 3733 Minimum Time to Complete All Deliveries
⊣ 3748 Count Stable Subarrays
⊣ 3814 Maximum Capacity Within Budget
⊣ 3824 Minimum K to Reduce Array Within Limit