Taro Logo

Super Egg Drop

Hard
Google logo
Google
1 view
Topics:
Dynamic Programming

You are given k identical eggs and you have access to a building with n floors labeled from 1 to n.

You know that there exists a floor f where 0 <= f <= n such that any egg dropped at a floor higher than f will break, and any egg dropped at or below floor f will not break.

Each move, you may take an unbroken egg and drop it from any floor x (where 1 <= x <= n). If the egg breaks, you can no longer use it. However, if the egg does not break, you may reuse it in future moves.

Return the minimum number of moves that you need to determine with certainty what the value of f is.

Example 1:

Input: k = 1, n = 2
Output: 2
Explanation: 
Drop the egg from floor 1. If it breaks, we know that f = 0.
Otherwise, drop the egg from floor 2. If it breaks, we know that f = 1.
If it does not break, then we know f = 2.
Hence, we need at minimum 2 moves to determine with certainty what the value of f is.

Example 2:

Input: k = 2, n = 6
Output: 3

Example 3:

Input: k = 3, n = 14
Output: 4

Constraints:

  • 1 <= k <= 100
  • 1 <= n <= 10^4

Solution


Egg Dropping Problem

The egg dropping problem is a classic dynamic programming problem. The goal is to find the minimum number of moves needed to determine the critical floor f with k eggs and n floors.

1. Naive Recursive Approach

A simple approach is to try every possible floor to drop the egg. For each floor x, there are two possibilities:

  • The egg breaks: We have k-1 eggs and x-1 floors to check.
  • The egg doesn't break: We have k eggs and n-x floors to check.

We want to minimize the worst-case scenario, so we take the maximum of these two possibilities and add 1 (for the current drop). We recursively apply this logic.

def egg_drop_recursive(k, n):
    if n == 0 or n == 1:
        return n
    if k == 1:
        return n

    min_attempts = float('inf')
    for x in range(1, n + 1):
        attempts = 1 + max(egg_drop_recursive(k - 1, x - 1),  # Egg breaks
                           egg_drop_recursive(k, n - x))  # Egg doesn't break
        min_attempts = min(min_attempts, attempts)
    return min_attempts
  • Time Complexity: O(2^n) in the worst case due to overlapping subproblems.
  • Space Complexity: O(n) due to the recursion depth.

This naive approach leads to exponential time complexity because the same subproblems are solved repeatedly.

2. Dynamic Programming Approach

To optimize, we use dynamic programming to store the results of subproblems and avoid recomputation. We create a 2D table dp[k][n] where dp[i][j] represents the minimum number of moves needed to find the critical floor with i eggs and j floors.

def egg_drop_dp(k, n):
    dp = [[0] * (n + 1) for _ in range(k + 1)]

    # If we have only one floor, one attempt is needed, if no floor - no attempts
    for i in range(1, k + 1):
        dp[i][1] = 1
        dp[i][0] = 0

    # If we have only one egg, we have to check every floor linearly
    for j in range(1, n + 1):
        dp[1][j] = j

    for i in range(2, k + 1):
        for j in range(2, n + 1):
            dp[i][j] = float('inf')
            for x in range(1, j + 1):
                attempts = 1 + max(dp[i - 1][x - 1],  # Egg breaks
                                   dp[i][j - x])  # Egg doesn't break
                dp[i][j] = min(dp[i][j], attempts)

    return dp[k][n]
  • Time Complexity: O(k * n^2) because we iterate through the dp table of size k * n and for each cell, we iterate up to n times.
  • Space Complexity: O(k * n) to store the dp table.

3. Optimized Dynamic Programming with Binary Search

The time complexity can be further optimized by using binary search instead of linear search to find the optimal floor x. The observation is that dp[i - 1][x - 1] is an increasing function with respect to x and dp[i][j - x] is a decreasing function with respect to x. Therefore, their maximum can be found using binary search.

def egg_drop_dp_binary_search(k, n):
    dp = [[0] * (n + 1) for _ in range(k + 1)]

    for i in range(1, k + 1):
        dp[i][1] = 1
        dp[i][0] = 0

    for j in range(1, n + 1):
        dp[1][j] = j

    for i in range(2, k + 1):
        for j in range(2, n + 1):
            low = 1
            high = j
            dp[i][j] = float('inf')
            while low <= high:
                mid = (low + high) // 2
                break_case = dp[i - 1][mid - 1]
                not_break_case = dp[i][j - mid]

                attempts = 1 + max(break_case, not_break_case)
                dp[i][j] = min(dp[i][j], attempts)

                if break_case < not_break_case:
                    low = mid + 1
                else:
                    high = mid - 1

    return dp[k][n]
  • Time Complexity: O(k * n * log(n)) due to the binary search within each cell of the dp table.
  • Space Complexity: O(k * n) to store the dp table.

4. Further Optimization (Space Optimization)

The space complexity can be further reduced to O(n) by using only two rows of the DP table at a time, since the current row only depends on the previous row.

Edge Cases

  • If n is 0 or 1, the answer is n.
  • If k is 1, the answer is n (we have to try each floor linearly).