Part 6 · Lesson 29 · 24 min read

Binary Search on the Answer

When the answer itself can be searched: square roots, eating speeds, shipping capacity.

On this page

  1. Searching for the answer itself
  2. Recognising the pattern
  3. The four-step recipe
  4. A first example: integer square root
  5. Minimum speed: Koko eating bananas
  6. Traced: finding Koko's speed
  7. Minimum capacity and the greedy check
  8. Maximising the minimum
  9. Practice questions (6)
  10. Make it stick
  11. Part 6 complete — what's next

Searching for the answer itself

Lesson 28 ended with a general idea: binary search finds the first point where a yes/no question changes from false to true. Binary search is a method that checks the middle of a range and throws away half of the range each time. Until now the question was about array positions. In this lesson the question is about candidate answers (possible answers that we want to test).

Example: “What is the slowest eating speed that finishes all the bananas in 8 hours?” Working out that speed directly is hard. But checking a guess is easy. At speed 5, add up the hours. And if speed 5 works, speed 6 works too. So the question “does speed k work?” is false for small k and true for large k. This is exactly the shape that binary search needs.

Recognising the pattern

Look for these three signs in the problem:

  • It asks for a minimum or maximum value: the smallest speed, the least capacity, the largest distance.
  • For a given guess, you can check whether it works in about O(n) time. Often this is a simple greedy pass. A greedy method always takes the best choice it can see right now, and never goes back.
  • The answer is monotonic. This means the results only change in one direction: if x works, every larger x works too (or every smaller x works).

The four-step recipe

Binary search on the answer
  1. Range: find the smallest and largest possible answers. These are lo and hi.
  2. Check: write a function works(x) that tells you if guess x is good. It is usually a single O(n) pass.
  3. Direction: confirm that works(x) gives false…false, true…true (or the reverse, true…true, false…false).
  4. Search: use binary search to find the first true (for a minimum) or the last true (for a maximum).

The time is O(n · log(range)). There are about log(range) guesses, and each guess is checked in O(n) time. Even a range of 109 needs only about 30 guesses.

A first example: integer square root

The integer square root of x is the largest whole number r with r × r ≤ x. For example, it is 2 for x = 8. The test “r × r > x” is false for small r and true for large r. Find the first r where it is true. The answer is one less than that r.

JavaScript
function mySqrt(x) {  // the last r with r * r <= x  =  (the first r with r * r > x) - 1  let lo = 0, hi = x + 1;  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (mid * mid > x) hi = mid;    else lo = mid + 1;  }  return lo - 1;}console.log(mySqrt(8));  // 2   (2 × 2 = 4 ≤ 8, 3 × 3 = 9 > 8)console.log(mySqrt(16)); // 4console.log(mySqrt(0));  // 0

Minimum speed: Koko eating bananas

Koko has piles of bananas and h hours. At speed k she eats up to k bananas per hour, from one pile only. A pile of 7 at speed 3 takes 3 hours (3 + 3 + 1 bananas). Find the slowest speed that finishes in time.

  • Range: 1 to the largest pile. At that speed, every pile takes exactly 1 hour.
  • Check: total hours = the sum of Math.ceil(pile / k) (Math.ceil rounds up). The speed works if the total is ≤ h.
  • Direction: a faster speed never takes longer, so the answers are false…false, true…true. Find the first true.

Traced: finding Koko's speed

step 1 / 10
Set up
lo = 1, hi = 11

The speed is at least 1. Speed 11 (the largest pile) always finishes in time, because each pile takes one hour.

1const piles = [3, 6, 7, 11], h = 8;2const hours = (k) => piles.reduce((t, p) => t + Math.ceil(p / k), 0);▶let lo = 1, hi = Math.max(...piles);4while (lo < hi) {5  const mid = lo + Math.floor((hi - lo) / 2);6  if (hours(mid) <= h) hi = mid;7  else lo = mid + 1;8}9console.log(lo);
Variables
piles[3, 6, 7, 11]
h8
lo1
hi11
Console
(nothing printed yet)
Each guess is checked with one pass over the piles. The result rules out half of the remaining speeds. · interactive
Dry run · hours needed at each speed for piles [3, 6, 7, 11]
Speed123456…11
Hours271510886…4
≤ 8?nononoyesyesyes…yes

The first “yes” is at speed 4. Binary search finds it without working out every column.

Minimum capacity and the greedy check

Packages must be shipped in the given order within a number of days. What is the smallest ship capacity (the most weight the ship can carry in one day) that works? The check is greedy: load packages in order, and start a new day only when the next package does not fit. Loading as much as possible each day never needs more days than any other way of loading.

JavaScript
function shipWithinDays(weights, days) {  // Greedy check: load packages in order. Start a new day when the next one does not fit.  const daysNeeded = (cap) => {    let d = 1, load = 0;    for (const w of weights) {      if (load + w > cap) { d++; load = 0; }      load += w;    }    return d;  };  let lo = Math.max(...weights);                     // must fit the heaviest package  let hi = weights.reduce((a, b) => a + b, 0);        // everything in one day  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (daysNeeded(mid) <= days) hi = mid;    else lo = mid + 1;  }  return lo;}console.log(shipWithinDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5)); // 15

The lower end of the range matters. The capacity must be at least the weight of the heaviest package, or that package could never be shipped. “Split an array into k parts so that the largest sum is as small as possible” is the same problem in different words (Practice question 5).

Maximising the minimum

Some problems ask for the largest value that works. An example is the largest possible smallest gap when you place balls in baskets. Now works(x) gives true…true, false…false, and you want the last true. Make two changes. First, set lo = mid when mid works. Second, round mid up, so that the range always gets smaller.

JavaScript
// Place m balls in baskets at the given positions so the smallest gap is as large as possible.function maxDistance(position, m) {  const pos = [...position].sort((a, b) => a - b);  const canPlace = (gap) => {                 // greedy: put each ball as far left as allowed    let count = 1, last = pos[0];    for (const p of pos) {      if (p - last >= gap) { count++; last = p; }    }    return count >= m;  };  // canPlace is true…true, false…false — find the LAST true  let lo = 1, hi = pos[pos.length - 1] - pos[0];  while (lo < hi) {    const mid = lo + Math.ceil((hi - lo) / 2);  // round UP, or lo = mid could make an endless loop    if (canPlace(mid)) lo = mid;    else hi = mid - 1;  }  return lo;}console.log(maxDistance([1, 2, 3, 4, 7], 3)); // 3   (balls at 1, 4, 7)
Dry run · first true vs last true
Goalworks(x) looks likeIf works(mid)Elsemid rounds
Minimum that worksF F F T T Thi = midlo = mid + 1down
Maximum that worksT T T F F Flo = midhi = mid − 1up

Practice questions

For every question, write the four recipe steps as comments before you write any code.

Q1Integer square rootEasy

Return the square root of a whole number x (0 or more), rounded down, without built-in power functions.

Example 1
Input: 4
Output: 2
Explanation: 2 × 2 = 4.
Example 2
Input: 8
Output: 2
Explanation: √8 ≈ 2.83; round down.
Stuck? Open one hint at a time
Hint 1
The answer is the last r with r × r ≤ x.
Show the solution with explanations
Approach 1Last true
The idea

Search r in [0, x]; “r × r ≤ x” is true…true, false…false.

JavaScript
function mySqrt(x) {  let lo = 0, hi = x;  while (lo < hi) {    const mid = lo + Math.ceil((hi - lo) / 2);    if (mid * mid <= x) lo = mid;    else hi = mid - 1;  }  return lo;}console.log(mySqrt(4));          // 2console.log(mySqrt(8));          // 2console.log(mySqrt(2147395599)); // 46339
How it works

O(log x) time. This is the “last true” form, so mid rounds up.

Which approach should you use?

Do not use Math.sqrt. The question tests whether you can do the search. (LeetCode 69.)

Q2Koko eating bananasMedium

Return the smallest eating speed k that lets Koko finish all piles within h hours.

Example 1
Input: piles = [3, 6, 7, 11], h = 8
Output: 4
Explanation: Traced in the lesson.
Example 2
Input: piles = [30, 11, 23, 4, 20], h = 5
Output: 30
Explanation: There are five piles and five hours, so Koko must finish one pile per hour. The speed must be at least the largest pile.
Stuck? Open one hint at a time
Hint 1
Range 1 … max(piles). Check: the sum of ceil(pile / k) must be ≤ h (ceil rounds up).
Show the solution with explanations
Approach 1First speed that works
The idea

The algorithm traced in the lesson.

JavaScript
function minEatingSpeed(piles, h) {  let lo = 1, hi = Math.max(...piles);  while (lo < hi) {    const k = lo + Math.floor((hi - lo) / 2);    let hours = 0;    for (const p of piles) hours += Math.ceil(p / k);    if (hours <= h) hi = k;    else lo = k + 1;  }  return lo;}console.log(minEatingSpeed([3, 6, 7, 11], 8));        // 4console.log(minEatingSpeed([30, 11, 23, 4, 20], 5));  // 30
How it works

O(n log m), where m is the largest pile.

Which approach should you use?

(LeetCode 875.)

Q3Smallest divisor given a thresholdMedium

Find the smallest positive divisor such that the sum of every value divided by it (each result rounded up) is at most threshold.

Example 1
Input: nums = [1, 2, 5, 9], threshold = 6
Output: 5
Explanation: With 5: 1 + 1 + 1 + 2 = 5 ≤ 6. With 4: 1 + 1 + 2 + 3 = 7 > 6.
Stuck? Open one hint at a time
Hint 1
This has the same shape as Koko. A bigger divisor gives a smaller sum.
Show the solution with explanations
Approach 1First divisor that works
The idea

Use the range 1 … max(nums). Check the sum of the divisions, each rounded up.

JavaScript
function smallestDivisor(nums, threshold) {  let lo = 1, hi = Math.max(...nums);  while (lo < hi) {    const d = lo + Math.floor((hi - lo) / 2);    const sum = nums.reduce((s, x) => s + Math.ceil(x / d), 0);    if (sum <= threshold) hi = d;    else lo = d + 1;  }  return lo;}console.log(smallestDivisor([1, 2, 5, 9], 6)); // 5
How it works

O(n log m) time. The whole question is to see that this is Koko in different words.

Which approach should you use?

(LeetCode 1283.)

Q4Capacity to ship packages within D daysMedium

Packages must ship in the given order. Return the smallest ship capacity that ships all of them within days days.

Example 1
Input: weights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], days = 5
Output: 15
Explanation: Days: [1–5], [6, 7], [8], [9], [10].
Example 2
Input: weights = [3, 2, 2, 4, 1, 4], days = 3
Output: 6
Explanation: Days: [3, 2], [2, 4], [1, 4].
Stuck? Open one hint at a time
Hint 1
Range: max(weights) … sum(weights). Check with the greedy loader.
Show the solution with explanations
Approach 1First capacity that works
The idea

The method from the lesson.

JavaScript
function shipWithinDays(weights, days) {  const need = (cap) => {    let d = 1, load = 0;    for (const w of weights) {      if (load + w > cap) { d++; load = 0; }      load += w;    }    return d;  };  let lo = Math.max(...weights), hi = weights.reduce((a, b) => a + b, 0);  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (need(mid) <= days) hi = mid;    else lo = mid + 1;  }  return lo;}console.log(shipWithinDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5)); // 15console.log(shipWithinDays([3, 2, 2, 4, 1, 4], 3));              // 6
How it works

O(n log(sum)) time.

Which approach should you use?

(LeetCode 1011.)

Q5Split array, minimise the largest sumHard

Split nums into k non-empty contiguous parts so that the largest part sum is as small as possible. Return that sum.

Example 1
Input: nums = [7, 2, 5, 10, 8], k = 2
Output: 18
Explanation: [7, 2, 5] and [10, 8]: the larger sum is 18, and no split does better.
Example 2
Input: nums = [1, 2, 3, 4, 5], k = 2
Output: 9
Explanation: [1, 2, 3] and [4, 5].
Stuck? Open one hint at a time
Hint 1
“k parts” = “k days”, “largest sum” = “capacity”. It is Question 4.
Show the solution with explanations
Approach 1Same as ship capacity
The idea

Find the smallest limit for which the greedy split needs at most k parts.

JavaScript
function splitArray(nums, k) {  const parts = (limit) => {    let p = 1, sum = 0;    for (const x of nums) {      if (sum + x > limit) { p++; sum = 0; }      sum += x;    }    return p;  };  let lo = Math.max(...nums), hi = nums.reduce((a, b) => a + b, 0);  while (lo < hi) {    const mid = lo + Math.floor((hi - lo) / 2);    if (parts(mid) <= k) hi = mid;    else lo = mid + 1;  }  return lo;}console.log(splitArray([7, 2, 5, 10, 8], 2)); // 18console.log(splitArray([1, 2, 3, 4, 5], 2));  // 9
How it works

O(n log(sum)) time. This is a hard problem, but it is easy once you see the pattern.

Which approach should you use?

(LeetCode 410.)

Q6Magnetic force between two ballsMedium

Place m balls in baskets at the given positions so that the smallest distance between any two balls is as large as possible. Return that distance.

Example 1
Input: position = [1, 2, 3, 4, 7], m = 3
Output: 3
Explanation: Balls at 1, 4 and 7: the gaps are 3 and 3.
Example 2
Input: position = [5, 4, 3, 2, 1, 1000000000], m = 2
Output: 999999999
Explanation: Put the two balls at the ends.
Stuck? Open one hint at a time
Hint 1
You want the largest possible minimum gap. Find the last gap size that still lets you place m balls with the greedy method.
Show the solution with explanations
Approach 1Last true, greedy placement
The idea

The method from the lesson.

JavaScript
function maxDistance(position, m) {  const pos = [...position].sort((a, b) => a - b);  const canPlace = (gap) => {    let count = 1, last = pos[0];    for (const p of pos) if (p - last >= gap) { count++; last = p; }    return count >= m;  };  let lo = 1, hi = pos[pos.length - 1] - pos[0];  while (lo < hi) {    const mid = lo + Math.ceil((hi - lo) / 2);    if (canPlace(mid)) lo = mid;    else hi = mid - 1;  }  return lo;}console.log(maxDistance([1, 2, 3, 4, 7], 3));                  // 3console.log(maxDistance([5, 4, 3, 2, 1, 1000000000], 2));      // 999999999
How it works
Dry run · canPlace for [1, 2, 3, 4, 7], m = 3
gapballs placed at≥ 3 balls?
21, 3, 7yes
31, 4, 7yes
41, 7no

Last true: 3.

Which approach should you use?

O(n log n + n log(range)) time. (LeetCode 1552.)

Make it stick

Make it stick — close this page, then
  1. List the three signs that a problem can be solved by binary search on the answer.
  2. State the four-step recipe and the overall time complexity.
  3. Write the Koko check function and say why the range is 1 to max(piles).
  4. Explain why “last true” searches round mid up.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

Part 6 complete — what's next

You can now use binary search on arrays, boundaries, rotated arrays and answers. When a problem says “sorted”, “minimum that works” or O(log n), think of this part.

Part 7 — Strings uses the techniques you know (two pointers, windows and frequency maps) on text: palindromes, prefixes, Roman numerals, compression and substring windows.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series