AniUI Academy

Binary Search, and the Search-on-Answer Pattern

Binary search proved properly, plus its most underrated form — searching over a range of possible ANSWERS rather than a sorted array, for problems that don't look like search at all.

10 min read

Binary search is the cleanest possible demonstration of O(log n), and it's worth proving properly once — but this lesson's real destination is a less commonly taught application of the same idea: searching over a range of possible answers, for problems that don't look anything like "search a sorted array" on the surface.

Binary search, proved

function binarySearch(sortedArr, target) {
  let low = 0;
  let high = sortedArr.length - 1;
 
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
 
    if (sortedArr[mid] === target) return mid;
    if (sortedArr[mid] < target) low = mid + 1;  // target must be to the right
    else high = mid - 1;                          // target must be to the left
  }
 
  return -1; // not found
}

Why this needs a sorted array, specifically: the entire algorithm rests on one guarantee. If sortedArr[mid] is smaller than the target, the target — if it exists at all — must be somewhere to the right, because sortedness guarantees everything to the left of mid is even smaller than sortedArr[mid] already was. That guarantee is what makes discarding an entire half of the array safe. On unsorted data, that guarantee is false — the target could be anywhere — and discarding a half could throw away the one place it actually was.

Complexity, proved the same way merge sort's recursion depth was proved several lessons ago: every comparison discards exactly half the remaining candidates. The number of times you can halve n before reaching a single candidate is log₂(n) — so binary search is O(log n), the same "repeated halving" argument, applied to narrowing a search range instead of splitting a sort's recursion.

The generalization: search on the space of answers

Here's the reframe that unlocks a whole category of problems: binary search's real requirement isn't "a sorted array" — it's a monotonic condition: as you move through the candidates in order, the answer to "does this candidate work" flips from false to true (or true to false) exactly once, never flip-flopping back and forth. A sorted array is one way to get that guarantee (values are ordered, so "is this element ≥ target" flips from false to true exactly once as you scan left to right) — but it's not the only way.

The problem: split a list of tasks across k workers, minimizing the maximum load any single worker gets assigned (a real load-balancing question — sharding work across web workers, say).

function canSplitWithMaxLoad(tasks, k, maxLoad) {
  let workersNeeded = 1;
  let currentLoad = 0;
 
  for (const task of tasks) {
    if (currentLoad + task > maxLoad) {
      workersNeeded++;
      currentLoad = task;
    } else {
      currentLoad += task;
    }
  }
 
  return workersNeeded <= k;
}
 
function minimizeMaxLoad(tasks, k) {
  let low = Math.max(...tasks);           // can't go below the single biggest task
  let high = tasks.reduce((a, b) => a + b, 0); // one worker taking everything
 
  while (low < high) {
    const mid = Math.floor((low + high) / 2);
    if (canSplitWithMaxLoad(tasks, k, mid)) {
      high = mid;       // mid works — try to do even better
    } else {
      low = mid + 1;    // mid doesn't work — need a bigger allowance
    }
  }
 
  return low;
}

There's no sorted array here at all — the thing being binary-searched is the candidate maximum load itself, ranging from "the single biggest task" (the floor — you can never go below what one task alone requires) to "the sum of everything" (one worker doing it all). The monotonic property that makes this valid: if a given maxLoad value is feasible (splits into k or fewer workers), every larger maxLoad is also feasible — feasibility never gets harder as the allowance grows. That's the exact same "everything on one side is uniformly one way, everything on the other side is uniformly the other" guarantee a sorted array gives, just expressed over a range of possible answers instead of array positions.

  1. Step 1

    Define the range of possible answers

    Lowest conceivable answer to highest conceivable answer — here, biggest single task to the sum of all tasks.

  2. Step 2

    Pick the midpoint candidate

    Same as picking mid in a normal binary search.

  3. Step 3

    Check feasibility, not equality

    Can this candidate answer actually work? (Here: does this maxLoad split into k or fewer workers?)

  4. Step 4

    Discard half the range

    Feasible → try smaller (search the lower half); infeasible → need bigger (search the upper half).

Search-on-answer: binary search over candidate answers, using a feasibility check instead of an array comparison.

The complexity accounting that's easy to miss

The number of iterations is still O(log(range)) — same halving argument. But each iteration's canSplitWithMaxLoad check isn't free — it's an O(m) scan over the m tasks. The honest total complexity is O(log(range) * m), not simply O(log n). This is worth stating explicitly because it's a common oversight: "I used binary search, so it's O(log n)" skips the real cost of whatever work each iteration's feasibility check actually does — the same care this course has applied to every other pattern (two pointers, sliding window, tree traversal) applies here too.

Try it yourself
Loading playground...

What to remember

  • Binary search requires sortedness specifically because it's what guarantees discarding half the array can never discard the target — and it achieves O(log n) by halving the remaining candidates every step.
  • The search-on-answer pattern generalizes binary search from "a sorted array" to "any monotonic feasibility condition over a range of candidate answers" — useful for optimization-flavored problems that don't look like search at all on the surface.
  • What makes search-on-answer valid is the same guarantee sortedness provides a normal binary search: feasibility (or the checked condition) must flip at most once as the candidate answer increases.
  • The honest complexity of search-on-answer is O(log(range) × cost of one feasibility check) — don't drop the second factor just because the first one is a satisfying O(log n).

Check yourself

4 questions · pass 3/4 to unlock Debouncing, Throttling, and the Right Complexity Framing

up to 50
  1. 1.Why does binary search require the input to be sorted, and what happens to its correctness if it's run on unsorted data?

  2. 2.What is the time complexity of binary search on a sorted array of n elements, and why?

  3. 3.The 'search on answer' pattern applies binary search to problems that don't look like searching a sorted array at all — like 'find the minimum possible maximum load when splitting a list of tasks across k workers.' What property must the space of POSSIBLE ANSWERS have for this pattern to apply?

  4. 4.In a 'search on answer' binary search (e.g. finding the minimum viable page size for a virtualized list to render within a time budget), what does each iteration's 'check' function typically need to do, and what does that make the overall complexity?

4 left to answer