DSA: Binary Search on Answer Pattern

8 min read6.7k

Solution to Last Week's Challenge

Last week's problem was: DSA: Prefix Sum Pattern (Real Use Cases)

The optimal solution uses dsa: prefix sum pattern (real use cases) pattern with careful state management.

Complexity Analysis:

  • Time Complexity: O(n)
  • Space Complexity: O(1)

Binary search is often one of the first algorithms we learn, typically introduced as a way to find a specific number in a sorted list in $O(\log n)$ time. However, in the context of high-level software engineering and competitive programming, its most powerful application isn't searching through arrays—it’s searching through the "answer space." This technique, known as Binary Search on Answer, is a cornerstone of optimization problems where we need to find the minimum or maximum value that satisfies a specific condition.

For a senior engineer, mastering this pattern is crucial because it transforms complex, seemingly non-linear problems into manageable logarithmic ones. Whether you are optimizing the throughput of a distributed system, determining the minimum buffer size for a streaming service, or solving a resource allocation problem under constraints, the "Binary Search on Answer" pattern provides a robust framework. It tests your ability to recognize monotonicity—the property where if a solution works for a value X, it also works for all values greater than X (or vice versa).

Theory: The Core Mechanics

The fundamental requirement for Binary Search on Answer is a monotonic predicate function. If you can define a function check(x) that returns a boolean, and the outputs of this function for a range of x look like [False, False, True, True, True], you have a monotonic relationship. The goal is to find the "boundary" where the state flips.

In standard binary search, we search indices. In this pattern, we search a range of values (e.g., from 1 to 1,000,000,000). The time complexity is $O(C \cdot \log(\text{Range}))$, where $C$ is the cost of the check function. Even if the range is massive, $\log(10^9)$ is only about 30, making this incredibly efficient.

Pattern Visualization

To solve these problems, we follow a structured workflow. The most difficult part is usually not the binary search itself, but defining the check function and identifying the correct search boundaries.

Implementation: Capacity To Ship Packages Within D Days

Let’s look at a classic interview problem: You have an array of weights and a number of days. You need to find the minimum ship capacity that allows you to transport all packages within the given days.

Brute Force Approach

The smallest possible capacity is the weight of the heaviest package (otherwise we can't carry it). The largest is the sum of all weights (shipping everything in one day). We could check every capacity in between.

python
# Brute Force: Linear Search
# Time: O(Range * N) where Range = sum(weights) - max(weights)
def shipWithinDaysBrute(weights, days):
    low = max(weights)
    high = sum(weights)
    
    for capacity in range(low, high + 1):
        # Check if this capacity works
        current_days = 1
        current_load = 0
        for w in weights:
            if current_load + w > capacity:
                current_days += 1
                current_load = 0
            current_load += w
        
        if current_days <= days:
            return capacity

Optimized Approach: Binary Search on Answer

Since if we can ship everything with capacity X, we can definitely do it with X + 1, the property is monotonic.

python
# Optimized: Binary Search on Answer
# Time: O(N * log(sum(weights)))
# Space: O(1)
def shipWithinDays(weights, days):
    def can_ship(capacity):
        # Predicate function: O(N)
        count_days = 1
        current_load = 0
        for w in weights:
            if current_load + w > capacity:
                count_days += 1
                current_load = w
            else:
                current_load += w
        return count_days <= days

    low = max(weights) # Must be at least the heaviest item
    high = sum(weights) # At most the sum of all items
    ans = high

    while low <= high:
        mid = (low + high) // 2
        if can_ship(mid):
            ans = mid     # This capacity works, try smaller
            high = mid - 1
        else:
            low = mid + 1 # Too small, increase capacity
            
    return ans

Complexity Analysis Table

ApproachTime ComplexitySpace ComplexityScalability
Brute Force$O(N \cdot \text{Range})$$O(1)$Poor; fails on large ranges
Binary Search on Answer$O(N \cdot \log(\text{Range}))$$O(1)$Excellent; handles $10^9$ ranges
Dynamic Programming$O(N \cdot \text{Days})$$O(N \cdot \text{Days})$Moderate; memory intensive

Common Patterns and Variations

1. Minimizing the Maximum (The "Split" Pattern)

Common in load balancing. Example: "Split Array Largest Sum."

python
# Goal: Minimize the maximum sum of a subarray
def check(max_sum_allowed):
    # Logic to count how many subarrays are needed 
    # if each sum is <= max_sum_allowed
    pass

2. Maximizing the Minimum (The "Distance" Pattern)

Common in physical placement. Example: "Aggressive Cows."

python
# Goal: Maximize the minimum distance between objects
def check(min_dist_allowed):
    # Logic to see if we can place K objects 
    # such that each is at least min_dist_allowed apart
    pass

Practice Problems Mapping

The following chart categorizes common problems based on their frequency in FAANG interviews and their relative difficulty.


This Week's Interview Challenge

Problem: Minimum Time to Repair Cars

You are given an integer array ranks representing the ranks of several mechanics. ranks[i] is the rank of the $i^{th}$ mechanic. A mechanic with rank $r$ can repair $n$ cars in $r \cdot n^2$ minutes. You are also given an integer totalCars, the total number of cars that need to be repaired.

Find the minimum time required to repair all totalCars. Note that all mechanics can work simultaneously.

Example 1:

  • Input: ranks = [4, 2, 3, 1], totalCars = 10
  • Output: 16
  • Explanation:
    • Mechanic 1 (rank 4) repairs 2 cars: $4 \cdot 2^2 = 16$ mins.
    • Mechanic 2 (rank 2) repairs 2 cars: $2 \cdot 2^2 = 8$ mins.
    • Mechanic 3 (rank 3) repairs 2 cars: $3 \cdot 2^2 = 12$ mins.
    • Mechanic 4 (rank 1) repairs 4 cars: $1 \cdot 4^2 = 16$ mins.
    • Total cars = 2+2+2+4 = 10. Max time is 16.

Example 2:

  • Input: ranks = [5, 1, 8], totalCars = 6
  • Output: 16

Constraints:

  • 1 <= ranks.length <= 10^5
  • 1 <= ranks[i] <= 100
  • 1 <= totalCars <= 10^6

Hints:

  1. If you are given a fixed amount of time T, can you calculate how many cars each mechanic can repair?
  2. For a mechanic with rank r, the number of cars $n$ they can repair in time $T$ is $n = \lfloor \sqrt{T/r} \rfloor$.
  3. As time T increases, the total number of cars repaired increases. This indicates monotonicity.
  4. What is the lower and upper bound for the time? (Lower: 1, Upper: min(ranks) * totalCars * totalCars).

Template Code:

python
class Solution:
    def repairCars(self, ranks: List[int], totalCars: int) -> int:
        def can_repair_in_time(time):
            # TODO: Implement the check logic
            pass
        
        # TODO: Implement binary search on answer
        return 0

Submit your solution and check back next week for the detailed answer!


Conclusion

Binary Search on Answer is a powerful paradigm shift. Instead of trying to build a solution incrementally, you hypothesize an answer and verify its feasibility. When you encounter problems involving "minimum of maximums," "maximum of minimums," or "least capacity," your first instinct should be to check for monotonicity. Master the check function, and you'll find that many "Hard" level problems become surprisingly approachable.

References: