Lesson 50 of 107 6 min

Problem: Koko Eating Bananas (Search on Answer)

Learn the "Search on Answer" technique to solve complex optimization problems using binary search.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Problem Statement

Mental Model

Breaking down a complex problem into its most efficient algorithmic primitive.

Koko loves to eat bananas. There are n piles of bananas. Koko can decide her eating speed of k bananas per hour. She wants to finish all bananas within h hours.

Return the minimum integer k such that she can eat all the bananas within h hours.

Approach: Search on Answer

The "Answer" (speed $k$) has a monotonic property:

  • If speed $k$ is sufficient, any speed $> k$ is also sufficient.
  • If speed $k$ is insufficient, any speed $< k$ is also insufficient.
  1. Define Range: The minimum speed is 1, and the maximum speed is the largest pile.
  2. Binary Search:
    • Pick a middle speed mid.
    • Calculate the total hours needed at speed mid.
    • If totalHours <= h: mid might be the answer, but try smaller speeds (right = mid).
    • Else: speed is too slow, try larger speeds (left = mid + 1).

Java Implementation

public int minEatingSpeed(int[] piles, int h) {
    int left = 1, right = 0;
    for (int p : piles) right = Math.max(right, p);
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canEatAll(piles, h, mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

private boolean canEatAll(int[] piles, int h, int speed) {
    long time = 0;
    for (int p : piles) {
        time += (p + speed - 1) / speed; // Ceiling division
    }
    return time <= h;
}

Complexity Analysis

  • Time Complexity: $O(n \log (\max(piles)))$. We binary search over the range of speeds.
  • Space Complexity: $O(1)$.

Interview Tips

  • Mention why we use long for time: "To prevent integer overflow if h is large."
  • Explain ceiling division: (a + b - 1) / b is a clean way to write ceil(a/b) in Java.

5. Verbal Interview Script (Staff Tier)

Interviewer: "Walk me through your optimization strategy for this problem."

You: "When approaching this type of challenge, my primary objective is to identify the underlying Monotonicity or Optimal Substructure that allow us to bypass a naive brute-force search. In my implementation of 'Problem: Koko Eating Bananas (Search on Answer)', I focused on reducing the time complexity by leveraging a Dynamic Programming state transition. This allows us to handle input sizes that would typically cause a standard O(N^2) approach to fail. Furthermore, I prioritized memory efficiency by using in-place modifications. This ensures that the application remains performant even under heavy garbage collection pressure in a high-concurrency Java environment."

6. Staff-Level Interview Follow-Ups

Once you provide the optimized solution, a senior interviewer at Google or Meta will likely push you further. Here is how to handle the most common follow-ups:

Follow-up 1: "How does this scale to a Distributed System?"

If the input data is too large to fit on a single machine (e.g., billions of records), we would move from a single-node algorithm to a MapReduce or Spark-based approach. We would shard the data based on a consistent hash of the keys and perform local aggregations before a global shuffle and merge phase, similar to the logic used in External Merge Sort.

Follow-up 2: "What are the Concurrency implications?"

In a multi-threaded Java environment, we must ensure that our state (e.g., the DP table or the frequency map) is thread-safe. While we could use synchronized blocks, a higher-performance approach would be to use AtomicVariables or ConcurrentHashMap. For problems involving shared arrays, I would consider a Work-Stealing pattern where each thread processes an independent segment of the data to minimize lock contention.

7. Performance Nuances (The Java Perspective)

  1. Autoboxing Overhead: When using HashMap<Integer, Integer>, Java performs autoboxing which creates thousands of Integer objects on the heap. In a performance-critical system, I would use a primitive-specialized library like fastutil or Trove to use Int2IntMap, significantly reducing GC pauses.
  2. Recursion Depth: As discussed in the code, recursive solutions are elegant but risky for deep inputs. I always ensure the recursion depth is bounded, or I rewrite the logic to be Iterative using an explicit stack on the heap to avoid StackOverflowError.

6. Staff-Level Verbal Masterclass (Communication)

Interviewer: "How would you defend this specific implementation in a production review?"

You: "In a mission-critical environment, I prioritize the Big-O efficiency of the primary data path, but I also focus on the Predictability of the system. In this implementation, I chose a recursive approach with memoization. While a recursive solution is more readable, I would strictly monitor the stack depth. If this were to handle skewed inputs, I would immediately transition to an explicit stack on the heap to avoid a StackOverflowError. From a memory perspective, I leverage primitive arrays to ensure that we minimize the garbage collection pauses (Stop-the-world) that typically plague high-throughput Java applications."

7. Global Scale & Distributed Pivot

When a problem like this is moved from a single machine to a global distributed architecture, the constraints change fundamentally.

  1. Data Partitioning: We would shard the input space using Consistent Hashing. This ensures that even if our dataset grows to petabytes, any single query only hits a small subset of our cluster, maintaining logarithmic lookup times.
  2. State Consistency: For problems involving state updates (like DP or Caching), we would use a Distributed Consensus protocol like Raft or Paxos to ensure that all replicas agree on the final state, even in the event of a network partition (The P in CAP theorem).

8. Performance Nuances (The Staff Perspective)

  1. Cache Locality: Accessing a 2D matrix in row-major order (reading [i][j] then [i][j+1]) is significantly faster than column-major order in modern CPUs due to L1/L2 cache pre-fetching. I always structure my loops to align with how the memory is physically laid out.
  2. Autoboxing and Generics: In Java, using List<Integer> instead of int[] can be 3x slower due to the overhead of object headers and constant wrapping. For the most performance-sensitive sections of this algorithm, I advocate for primitive specialized structures.

Key Takeaways

  • If speed $k$ is sufficient, any speed $> k$ is also sufficient.
  • If speed $k$ is insufficient, any speed $< k$ is also insufficient.
  • Pick a middle speed mid.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.