Binary Search on Answer
Binary search doesn’t require an array at all. Any time a problem asks you to minimize the maximum (or maximize the minimum) of something, and “can we achieve X?” gets easier to answer as X grows, you can binary search directly over the space of possible answers — no array indices in sight.
What you’ll learn
- How to recognize the cue: “minimize the maximum” / “maximize the minimum”, with a feasibility check that gets monotonically easier or harder.
- The reusable template: binary search over
[lo, hi][lo, hi]guided by afeasible(mid)feasible(mid)predicate, instead ofarr[mid]arr[mid]. - Three worked shapes of the same pattern: Capacity To Ship Packages Within D Days, Koko Eating Bananas, and Split Array Largest Sum.
- Why it costs , and the one design step that makes or breaks it: proving your predicate is actually monotonic.
The cue: minimize the maximum, maximize the minimum
Look for a hidden number line of candidate answers where one side is
always “too small/slow/tight” and the other side is always “big/generous
enough” — with a clean cutoff in between. That cutoff is your answer, and a
feasible(candidate)feasible(candidate) function tells you which side of it you’re on.
Capacity To Ship Packages Within D Days: a conveyor belt ships
weightsweights in order, loading as many consecutive packages as fit under a
daily capacitycapacity before starting a new day. Find the minimum capacitycapacity
that ships everything within daysdays days.
As capacitycapacity grows, the number of days needed only ever decreases (or
stays the same) — never increases. That monotonic relationship is exactly
what binary search needs, even though there’s no array of “capacities” to
search — just the range of integers from the largest single package up to
the sum of everything.
The pattern: binary search over a feasibility predicate
def ship_within_days(weights, days):
def days_needed(capacity):
# Greedily pack today's shipment; start a new day when the next
# package would overflow the current one.
days_used = 1
current_load = 0
for w in weights:
if current_load + w > capacity:
days_used += 1
current_load = 0
current_load += w
return days_used
lo, hi = max(weights), sum(weights) # answer must be in [largest single package, ship it all in one day]
while lo < hi:
mid = lo + (hi - lo) // 2
if days_needed(mid) <= days:
hi = mid # capacity=mid WORKS -- try a smaller (tighter) capacity
else:
lo = mid + 1 # capacity=mid too small -- need more room per day
return lo
weights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(ship_within_days(weights, 5)) # expect 15def ship_within_days(weights, days):
def days_needed(capacity):
# Greedily pack today's shipment; start a new day when the next
# package would overflow the current one.
days_used = 1
current_load = 0
for w in weights:
if current_load + w > capacity:
days_used += 1
current_load = 0
current_load += w
return days_used
lo, hi = max(weights), sum(weights) # answer must be in [largest single package, ship it all in one day]
while lo < hi:
mid = lo + (hi - lo) // 2
if days_needed(mid) <= days:
hi = mid # capacity=mid WORKS -- try a smaller (tighter) capacity
else:
lo = mid + 1 # capacity=mid too small -- need more room per day
return lo
weights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(ship_within_days(weights, 5)) # expect 15The shape never changes: pick bounds you’re certain bracket the answer,
write a feasible(mid)feasible(mid) check, and narrow with lo < hilo < hi exactly like the
lower_boundlower_bound template — except midmid is a candidate answer, not an array
index.
How it works
Worked example: Koko Eating Bananas
Same shape, different feasibility check. Koko eats at most speedspeed bananas
per pile per hour; find the minimum speedspeed that finishes every pile
within hh hours.
import math
def min_eating_speed(piles, h):
def hours_needed(speed):
return sum(math.ceil(pile / speed) for pile in piles)
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi - lo) // 2
if hours_needed(mid) <= h:
hi = mid # speed=mid is fast enough -- try slower
else:
lo = mid + 1 # speed=mid too slow -- need faster
return lo
print(min_eating_speed([3, 6, 7, 11], 8)) # expect 4import math
def min_eating_speed(piles, h):
def hours_needed(speed):
return sum(math.ceil(pile / speed) for pile in piles)
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi - lo) // 2
if hours_needed(mid) <= h:
hi = mid # speed=mid is fast enough -- try slower
else:
lo = mid + 1 # speed=mid too slow -- need faster
return lo
print(min_eating_speed([3, 6, 7, 11], 8)) # expect 4And Split Array Largest Sum — split numsnums into mm contiguous
subarrays, minimizing the largest subarray sum — reuses the exact greedy
feasibility check from the shipping problem, just renamed:
def split_array_largest_sum(nums, m):
def pieces_needed(max_sum):
pieces = 1
current_sum = 0
for x in nums:
if current_sum + x > max_sum:
pieces += 1
current_sum = 0
current_sum += x
return pieces
lo, hi = max(nums), sum(nums)
while lo < hi:
mid = lo + (hi - lo) // 2
if pieces_needed(mid) <= m:
hi = mid
else:
lo = mid + 1
return lo
print(split_array_largest_sum([7, 2, 5, 10, 8], 2)) # expect 18def split_array_largest_sum(nums, m):
def pieces_needed(max_sum):
pieces = 1
current_sum = 0
for x in nums:
if current_sum + x > max_sum:
pieces += 1
current_sum = 0
current_sum += x
return pieces
lo, hi = max(nums), sum(nums)
while lo < hi:
mid = lo + (hi - lo) // 2
if pieces_needed(mid) <= m:
hi = mid
else:
lo = mid + 1
return lo
print(split_array_largest_sum([7, 2, 5, 10, 8], 2)) # expect 18Three different problem statements, the same eleven lines of binary search
scaffolding, and only the feasiblefeasible/hours_neededhours_needed/pieces_neededpieces_needed helper
changes.
Time and space complexity
| Operation | Complexity |
|---|---|
| Feasibility check (one pass over the input) | |
| Binary search over the answer range | iterations |
| Total | |
| Space | extra |
rangerange is hi - lohi - lo in your chosen bounds — for Koko it’s max(piles)max(piles),
for shipping/splitting it’s sum(nums) - max(nums)sum(nums) - max(nums).
When to use it
- The problem asks to minimize a maximum or maximize a minimum (capacity, speed, largest chunk, smallest gap) subject to a constraint.
- You can write a
feasible(candidate)feasible(candidate)check, typically a single greedy pass, and you can argue it’s monotonic incandidatecandidate. - The “search space” of candidate answers is large enough that trying every
value would be too slow, but bounded enough to pick honest
lolo/hihi. - Classic tell for “maximize the minimum spacing” variants (aggressive cows / minimize max distance between chosen points): the predicate becomes “can we place everything with at least this much spacing?”, and larger spacing only ever gets harder to satisfy.
Practice — real LeetCode problems
Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 875 — Koko Eating Bananas · Medium
Problem. Koko eats at a speed of kk bananas per hour, taking a whole hour per
pile even if the pile has fewer than kk left. Return the minimum kk that lets her
finish all piles within hh hours.
Constraints. 1 <= len(piles) <= 10^41 <= len(piles) <= 10^4, piles.length <= h <= 10^9piles.length <= h <= 10^9,
1 <= piles[i] <= 10^91 <= piles[i] <= 10^9.
Examples. piles = [3,6,7,11], h = 8piles = [3,6,7,11], h = 8 gives 44 ·
piles = [30,11,23,4,20], h = 5piles = [30,11,23,4,20], h = 5 gives 3030 · h = 6h = 6 gives 2323
Editorial
The array is not what you search. The answer space is, and the enabling property
is that feasibility is monotone: if speed kk finishes in time, every speed above
kk does too. That turns “find the minimum feasible” into a boundary binary search.
Time . Space .
Three details:
hi = max(piles)hi = max(piles). No speed above the largest pile helps, since each pile already takes exactly one hour at that speed.- Ceiling division.
(p + k - 1) // k(p + k - 1) // kkeeps everything in integers.math.ceil(p / k)math.ceil(p / k)goes through a float and can misround for values near . - The boundary shape (
while lo < hiwhile lo < hi,hi = midhi = mid) is required, becausemidmidmay itself be the answer. The exact-search shape would skip it.
([1000000000], 2)([1000000000], 2) giving 500000000500000000 confirms the arithmetic scales — a linear scan
over speeds would be iterations.
Follow-ups: “How do you know the range?” — 11 is the slowest meaningful speed and
max(piles)max(piles) the fastest useful one. “Prove monotonicity” — more speed never needs
more hours; this justifies the search. “Ship packages in DD days (LC 1011)?” — the
identical shape with a different feasibility function. “What if piles could be
consumed partially across hours?” — the ceiling disappears and it becomes plain
division.
LC 1011 — Capacity To Ship Packages Within D Days · Medium
Problem. Packages must be shipped in order within daysdays days. Return the
minimum ship capacity that makes this possible.
Constraints. 1 <= days <= len(weights) <= 5 * 10^41 <= days <= len(weights) <= 5 * 10^4,
1 <= weights[i] <= 5001 <= weights[i] <= 500.
Examples. weights = [1,2,3,4,5,6,7,8,9,10], days = 5weights = [1,2,3,4,5,6,7,8,9,10], days = 5 gives 1515 ·
weights = [3,2,2,4,1,4], days = 3weights = [3,2,2,4,1,4], days = 3 gives 66 ·
weights = [1,2,3,1,1], days = 4weights = [1,2,3,1,1], days = 4 gives 33
Editorial
Structurally identical to LC 875: binary search a candidate answer, with a greedy feasibility check.
Time . Space .
The bounds carry real meaning here:
lo = max(weights)lo = max(weights). A capacity below the heaviest package makes shipping impossible at any number of days, so the greedy check would loop forever. Starting atmaxmaxguarantees every candidate is at least achievable.hi = sum(weights)hi = sum(weights). One day is always enough at that capacity, so the answer cannot exceed it.
The greedy day-packing is optimal because the order is fixed: with no freedom to reorder, filling each day as much as possible can never require more days than any other valid packing.
([1,2,3,1,1], 4)([1,2,3,1,1], 4) giving 33 is worth tracing: capacity 3 packs as
[1,2] [3] [1,1][1,2] [3] [1,1] — three days, within the budget of four.
Follow-ups: “Why is greedy packing optimal?” — the fixed-order argument above.
“Split an array into kk parts minimising the largest sum (LC 410)?” — the same
problem restated. “What if packages could be reordered?” — it becomes bin packing,
which is NP-hard.
LC 410 — Split Array Largest Sum · Hard
Problem. Split numsnums into kk non-empty contiguous subarrays, minimising
the largest subarray sum. Return that minimum.
Constraints. 1 <= len(nums) <= 10001 <= len(nums) <= 1000, 0 <= nums[i] <= 10^60 <= nums[i] <= 10^6,
1 <= k <= min(50, len(nums))1 <= k <= min(50, len(nums)).
Examples. nums = [7,2,5,10,8], k = 2nums = [7,2,5,10,8], k = 2 gives 1818 ·
nums = [1,2,3,4,5], k = 2nums = [1,2,3,4,5], k = 2 gives 99 · nums = [1,4,4], k = 3nums = [1,4,4], k = 3 gives 44
Editorial
This is the same problem as LC 1011 with different nouns: “capacity” becomes “the largest allowed subarray sum”, and “days” becomes “parts”. The code is character-for-character the same greedy-plus-binary-search.
Time . Space .
[7,2,5,10,8][7,2,5,10,8] with k = 2k = 2 gives 1818: the split is [7,2,5][7,2,5] and [10,8][10,8], sums
1414 and 1818. No split of this array into two parts does better.
Recognising the equivalence is the point. Once you see “minimise the maximum, over contiguous groups”, the template applies regardless of the story wrapped around it.
There is also a genuine DP solution — dp[i][j]dp[i][j] = the best split of the first ii
elements into jj parts — at . It is worth naming, because it is what you
would reach for if the answer space were not monotone. Binary search on the answer is
better because feasibility here is monotone.
Follow-ups: “How is this the same as LC 1011?” — the mapping above; the most likely question. “Do it with DP?” — ; mention it as the general fallback. “What if the subarrays need not be contiguous?” — much harder, and the greedy check collapses. “Maximise the minimum instead?” — mirror the comparison, as in Sweep Line-adjacent allocation problems.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 875 | Koko Eating Bananas | Medium | Binary search on the minimum feasible eating speed |
| 1011 | Capacity To Ship Packages Within D Days | Medium | Binary search on the minimum feasible daily capacity |
| 410 | Split Array Largest Sum | Hard | Binary search on the minimum feasible “largest subarray sum”, using the identical greedy-count predicate |
| 774 | Minimize Max Distance to Gas Station | Hard · Premium | Style spacing problems — binary search on the maximum spacing such that everything still fits, with a “can I place all of them with at least this much room?” feasibility check |
| — | Aggressive Cows (classic CP) | — | Style spacing problems — binary search on the maximum spacing such that everything still fits, with a “can I place all of them with at least this much room?” feasibility check |
Recap
- The cue is “minimize the maximum” / “maximize the minimum” with a feasibility check that only gets easier (or only harder) as the candidate answer grows.
- The template is always the same eleven-ish lines: honest
lolo/hihibounds,while lo < hiwhile lo < hi,feasible(mid)feasible(mid)decideshi = midhi = midorlo = mid + 1lo = mid + 1. - Cost is — one feasibility pass per binary search step.
- The one thing that can silently break this pattern: a feasibility check that isn’t actually monotonic. Prove monotonicity in words before you trust the binary search around it.
Next: Monotonic Stack — maintaining an increasing or decreasing stack to answer “next greater/smaller” questions in a single pass.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
