Egg Drop Problem: Minimum Trials DP

The egg drop problem is a classic dynamic programming challenge that appears in technical interviews and competitive programming. Here's the setup: you have `n` identical eggs and a building with `k`...

Key Insights

  • The egg drop problem exposes why binary search fails with limited resources—you must balance worst-case outcomes across two different scenarios (egg breaks vs. survives), not just minimize average case.
  • The classic O(n × k²) DP solution can be optimized to O(n × k × log k) using binary search on the monotonic property of the subproblem results.
  • Reformulating the problem inversely—“how many floors can I check with t trials and n eggs?"—yields an elegant O(n × k) solution that’s both faster and more intuitive.

Introduction & Problem Statement

The egg drop problem is a classic dynamic programming challenge that appears in technical interviews and competitive programming. Here’s the setup: you have n identical eggs and a building with k floors. There exists a critical floor f such that eggs dropped from any floor above f will break, while eggs dropped from floor f or below will survive intact.

Your goal is to determine the minimum number of trials needed to find this critical floor in the worst case. The constraints are straightforward: broken eggs cannot be reused, but surviving eggs can be dropped again.

This problem has practical applications beyond puzzles. Think about testing fragile systems where each test might destroy the component, A/B testing with limited rollback capacity, or any scenario where you’re searching for a threshold but each probe has a cost. The egg drop problem formalizes the tradeoff between aggressive searching and resource preservation.

Why Greedy and Binary Search Fail

Your first instinct might be binary search. With unlimited eggs, binary search finds the critical floor in ⌈log₂(k)⌉ trials. But what happens when eggs are limited?

Consider 1 egg and 100 floors. If you try binary search and drop from floor 50, the egg might break. Now you have zero eggs and still need to check floors 1-49. Game over—you can’t determine the critical floor.

With a single egg, you’re forced into linear search: start at floor 1, move up one floor at a time. Worst case requires 100 trials.

def naive_binary_search(eggs: int, floors: int) -> int:
    """
    This approach FAILS with limited eggs.
    Demonstrates why binary search alone doesn't work.
    """
    if eggs == 1:
        # Forced into linear search
        return floors
    
    # Binary search assumes we can always split
    # But if egg breaks, we lose the ability to check upper half
    trials = 0
    low, high = 1, floors
    
    while low < high:
        mid = (low + high) // 2
        trials += 1
        # Problem: we don't know if egg breaks!
        # If it does, we can't continue binary search
        # This logic is fundamentally flawed for limited eggs
        high = mid
    
    return trials  # Incorrect for eggs < log2(floors)


# Example showing the failure
print(f"Binary search claims: {naive_binary_search(2, 100)} trials")
print(f"But with 2 eggs, we need to be smarter about worst-case scenarios")

The fundamental issue: binary search optimizes for the average case, but we need to minimize the worst case while accounting for resource depletion.

Recursive Solution and Subproblem Structure

The key insight is recognizing the optimal substructure. When you drop an egg from floor x:

  1. Egg breaks: The critical floor is below x. You now have n-1 eggs and must search floors 1 to x-1.
  2. Egg survives: The critical floor is at or above x. You still have n eggs and must search floors x+1 to k.

Since we want the worst-case minimum, we take the maximum of these two outcomes (worst case), then minimize over all possible floor choices.

The recurrence relation:

dp(n, k) = 1 + min(max(dp(n-1, x-1), dp(n, k-x))) for x in [1, k]

Here’s the recursive implementation with memoization:

from functools import lru_cache

def egg_drop_recursive(n: int, k: int) -> int:
    """
    Find minimum trials to determine critical floor.
    n: number of eggs
    k: number of floors
    Returns: minimum trials in worst case
    """
    @lru_cache(maxsize=None)
    def dp(eggs: int, floors: int) -> int:
        # Base cases
        if floors <= 1:
            return floors  # 0 floors = 0 trials, 1 floor = 1 trial
        if eggs == 1:
            return floors  # Must do linear search
        
        min_trials = float('inf')
        
        # Try dropping from each floor
        for x in range(1, floors + 1):
            # Egg breaks: check floors below (eggs-1, x-1)
            breaks = dp(eggs - 1, x - 1)
            # Egg survives: check floors above (eggs, floors-x)
            survives = dp(eggs, floors - x)
            # Worst case for this choice
            worst_case = 1 + max(breaks, survives)
            min_trials = min(min_trials, worst_case)
        
        return min_trials
    
    return dp(n, k)


# Test cases
print(f"2 eggs, 10 floors: {egg_drop_recursive(2, 10)} trials")  # 4
print(f"2 eggs, 100 floors: {egg_drop_recursive(2, 100)} trials")  # 14
print(f"3 eggs, 100 floors: {egg_drop_recursive(3, 100)} trials")  # 9

This solution is correct but has O(n × k²) time complexity due to the linear search for optimal floor at each state.

Bottom-Up DP Implementation

Converting to iterative DP eliminates recursion overhead and makes the solution more predictable in terms of memory usage:

def egg_drop_dp(n: int, k: int) -> int:
    """
    Bottom-up DP solution for egg drop problem.
    Time: O(n * k^2), Space: O(n * k)
    """
    # dp[i][j] = min trials for i eggs and j floors
    dp = [[0] * (k + 1) for _ in range(n + 1)]
    
    # Base case: 1 egg requires linear search
    for j in range(k + 1):
        dp[1][j] = j
    
    # Base case: 1 floor needs 1 trial, 0 floors needs 0
    for i in range(1, n + 1):
        dp[i][0] = 0
        dp[i][1] = 1
    
    # Fill the table
    for i in range(2, n + 1):       # eggs
        for j in range(2, k + 1):   # floors
            dp[i][j] = float('inf')
            
            for x in range(1, j + 1):  # try each floor
                # breaks: dp[i-1][x-1], survives: dp[i][j-x]
                worst = 1 + max(dp[i - 1][x - 1], dp[i][j - x])
                dp[i][j] = min(dp[i][j], worst)
    
    return dp[n][k]


print(f"2 eggs, 100 floors: {egg_drop_dp(2, 100)} trials")  # 14

Here’s where it gets interesting. Notice that as x increases:

  • dp[n-1][x-1] (egg breaks case) increases
  • dp[n][k-x] (egg survives case) decreases

This monotonic property means we can binary search for the optimal floor instead of trying all possibilities:

def egg_drop_optimized(n: int, k: int) -> int:
    """
    Optimized DP with binary search for floor selection.
    Time: O(n * k * log k), Space: O(n * k)
    """
    dp = [[0] * (k + 1) for _ in range(n + 1)]
    
    for j in range(k + 1):
        dp[1][j] = j
    
    for i in range(1, n + 1):
        dp[i][0] = 0
        dp[i][1] = 1
    
    for i in range(2, n + 1):
        for j in range(2, k + 1):
            # Binary search for optimal floor
            low, high = 1, j
            result = j  # worst case: linear search
            
            while low <= high:
                mid = (low + high) // 2
                
                breaks = dp[i - 1][mid - 1]    # egg breaks
                survives = dp[i][j - mid]       # egg survives
                
                # We want to find where breaks >= survives
                # That's the crossover point minimizing max(breaks, survives)
                if breaks >= survives:
                    result = min(result, 1 + breaks)
                    high = mid - 1
                else:
                    result = min(result, 1 + survives)
                    low = mid + 1
            
            dp[i][j] = result
    
    return dp[n][k]


print(f"2 eggs, 100 floors: {egg_drop_optimized(2, 100)} trials")  # 14
print(f"3 eggs, 1000 floors: {egg_drop_optimized(3, 1000)} trials")  # 19

Alternative DP Formulation: The Inverse Problem

The most elegant solution inverts the question: given t trials and n eggs, what’s the maximum number of floors we can check?

Let f(t, n) be this maximum. When we drop an egg:

  • If it breaks, we can check f(t-1, n-1) floors below
  • If it survives, we can check f(t-1, n) floors above
  • Plus the current floor itself

This gives us: f(t, n) = f(t-1, n-1) + f(t-1, n) + 1

def egg_drop_inverse(n: int, k: int) -> int:
    """
    Inverse formulation: find minimum t such that f(t, n) >= k
    Time: O(n * t) where t = O(k), Space: O(n)
    In practice, t is much smaller than k.
    """
    # dp[j] = max floors checkable with current trials and j eggs
    dp = [0] * (n + 1)
    
    trials = 0
    while dp[n] < k:
        trials += 1
        # Update from right to left to avoid using updated values
        new_dp = [0] * (n + 1)
        for j in range(1, n + 1):
            # f(t,j) = f(t-1,j-1) + f(t-1,j) + 1
            new_dp[j] = dp[j - 1] + dp[j] + 1
        dp = new_dp
    
    return trials


# Space-optimized version
def egg_drop_inverse_optimized(n: int, k: int) -> int:
    """
    Further optimized with single array.
    Time: O(n * t), Space: O(n)
    """
    dp = [0] * (n + 1)
    trials = 0
    
    while dp[n] < k:
        trials += 1
        # Process right to left
        for j in range(n, 0, -1):
            dp[j] = dp[j - 1] + dp[j] + 1
    
    return trials


print(f"2 eggs, 100 floors: {egg_drop_inverse_optimized(2, 100)} trials")  # 14
print(f"3 eggs, 1000 floors: {egg_drop_inverse_optimized(3, 1000)} trials")  # 19
print(f"10 eggs, 10000 floors: {egg_drop_inverse_optimized(10, 10000)} trials")  # 14

This approach runs in O(n × t) time where t is the answer. Since t ≤ k, worst case is still O(n × k), but in practice t is much smaller (logarithmic in k when eggs are plentiful).

Complexity Analysis and Practical Considerations

Approach Time Complexity Space Complexity
Recursive with memo O(n × k²) O(n × k)
Bottom-up DP O(n × k²) O(n × k)
Binary search optimized O(n × k × log k) O(n × k)
Inverse formulation O(n × t) ≈ O(n × k) O(n)

Edge cases to handle:

  • 1 egg: Always returns k (linear search required)
  • 0 floors: Returns 0 (nothing to check)
  • eggs ≥ log₂(floors): Binary search is optimal, answer is ⌈log₂(k)⌉

For production code, I recommend the inverse formulation. It’s faster in practice, uses minimal memory, and the logic is more intuitive once you understand the reformulation.

The egg drop problem teaches a valuable lesson: sometimes the best way to solve a problem is to ask a different question entirely.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.