887. Super Egg Drop
Problem Description
You have k
identical eggs and a building with n
floors (numbered from 1 to n). There exists a critical floor f
(where 0 ≤ f ≤ n
) with the following properties:
- Any egg dropped from a floor higher than
f
will break - Any egg dropped from floor
f
or below will not break
In each move, you can:
- Take an unbroken egg and drop it from any floor
x
(where1 ≤ x ≤ n
) - If the egg breaks, you cannot use it again
- If the egg doesn't break, you can reuse it in future moves
Your goal is to find the minimum number of moves required to determine the exact value of f
with certainty (in the worst case).
For example, if you have 2 eggs and 10 floors:
- You need to find a strategy that guarantees finding the critical floor
f
- The strategy should minimize the maximum number of drops needed in the worst-case scenario
- You must be able to determine
f
exactly, even if all but one egg breaks
The challenge is to develop an optimal strategy that balances:
- Using eggs efficiently (since broken eggs cannot be reused)
- Minimizing the total number of drops needed in the worst case
- Ensuring you can always determine
f
regardless of where it actually is
Intuition
Think about this problem as a decision tree where each node represents dropping an egg from a certain floor. When we drop an egg from floor x
:
- If it breaks, we know
f
must be belowx
, and we lose one egg - If it doesn't break, we know
f
is at or abovex
, and we keep all eggs
The key insight is that this is a minimax problem. We want to minimize the maximum number of moves needed in the worst case. At each step, we need to consider: what's the worst that could happen after dropping from floor x
?
Let's define dfs(i, j)
as the minimum number of moves needed to determine f
with certainty when we have i
floors to check and j
eggs available.
When we drop an egg from floor mid
(where mid
is between 1 and i
):
- If egg breaks: We have
mid - 1
floors below to check withj - 1
eggs - If egg doesn't break: We have
i - mid
floors above to check withj
eggs (same number of eggs)
Since we don't know which case will happen, we must prepare for the worst: max(dfs(mid - 1, j - 1), dfs(i - mid, j)) + 1
But which floor should we choose to drop from? We want to minimize this worst-case scenario. The brilliant observation is that:
dfs(mid - 1, j - 1)
increases asmid
increases (more floors to check below if egg breaks)dfs(i - mid, j)
decreases asmid
increases (fewer floors to check above if egg doesn't break)
Since one function increases and the other decreases, their maximum is minimized when they're approximately equal! This is why we use binary search to find the optimal dropping floor - we're looking for the point where these two values are closest to each other.
The binary search finds the floor where dfs(mid - 1, j - 1) ≤ dfs(i - mid, j)
transitions to dfs(mid - 1, j - 1) > dfs(i - mid, j)
, which gives us the optimal balance between the two worst-case scenarios.
Base cases are intuitive:
- If
i < 1
: No floors to check, return 0 - If
j = 1
: Only one egg, must check linearly from bottom, returni
Learn more about Math, Binary Search and Dynamic Programming patterns.
Solution Approach
The solution implements a dynamic programming approach with memoization using a recursive function dfs(i, j)
that represents the minimum number of moves needed to determine the critical floor with i
floors to check and j
eggs available.
Implementation Details:
-
Main Function Setup:
- The function
dfs(n, k)
is called wheren
is the number of floors andk
is the number of eggs - The
@cache
decorator is used for memoization to avoid recalculating the same subproblems
- The function
-
Base Cases:
if i < 1: return 0 # No floors to check if j == 1: return i # Only one egg left, must check linearly
-
Binary Search for Optimal Floor:
l, r = 1, i while l < r: mid = (l + r + 1) >> 1 # Equivalent to (l + r + 1) // 2 a = dfs(mid - 1, j - 1) # Egg breaks case b = dfs(i - mid, j) # Egg doesn't break case if a <= b: l = mid else: r = mid - 1
The binary search finds the floor
l
where:dfs(l - 1, j - 1) ≤ dfs(i - l, j)
butdfs(l, j - 1) > dfs(i - l - 1, j)
This is the point where the two worst-case scenarios are most balanced.
-
Computing the Result:
return max(dfs(l - 1, j - 1), dfs(i - l, j)) + 1
After finding the optimal floor
l
, we compute:- The worst case between egg breaking (
l - 1
floors withj - 1
eggs) and not breaking (i - l
floors withj
eggs) - Add 1 for the current drop
- The worst case between egg breaking (
Why Binary Search Works:
The key insight is that as we increase the dropping floor from 1 to i
:
dfs(mid - 1, j - 1)
monotonically increases (more floors to check if egg breaks)dfs(i - mid, j)
monotonically decreases (fewer floors to check if egg doesn't break)
The optimal floor is where these two values are closest, minimizing their maximum. The binary search efficiently finds this crossover point.
Time Complexity: O(k * n * log n)
where we have at most O(k * n)
unique states, and each state requires O(log n)
time for binary search.
Space Complexity: O(k * n)
for the memoization cache storing all possible states.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 2 eggs and 6 floors to illustrate the solution approach.
Problem Setup: We need to find the minimum number of moves to determine the critical floor f
in the worst case.
Step 1: Initial Call
- Call
dfs(6, 2)
- we have 6 floors to check and 2 eggs available
Step 2: Binary Search for Optimal Floor
- We need to find the best floor to drop from among floors 1-6
- Initialize:
l = 1, r = 6
First Binary Search Iteration:
mid = (1 + 6 + 1) / 2 = 4
- Calculate:
a = dfs(3, 1)
(if egg breaks at floor 4: check floors 1-3 with 1 egg)b = dfs(2, 2)
(if egg doesn't break: check floors 5-6 with 2 eggs)
Let's evaluate these recursive calls:
dfs(3, 1) = 3
(with 1 egg, must check linearly: worst case is 3 drops)dfs(2, 2)
needs further calculation...
Computing dfs(2, 2):
- Binary search:
l = 1, r = 2
mid = (1 + 2 + 1) / 2 = 2
a = dfs(1, 1) = 1
(1 floor with 1 egg)b = dfs(0, 2) = 0
(0 floors to check)- Since
a > b
, setr = 1
- Final: drop from floor 1
- Result:
max(dfs(0, 1), dfs(1, 2)) + 1 = max(0, 1) + 1 = 2
Back to Main Calculation:
- Now we know:
a = dfs(3, 1) = 3
andb = dfs(2, 2) = 2
- Since
a > b
, setr = 3
Second Binary Search Iteration:
l = 1, r = 3
mid = (1 + 3 + 1) / 2 = 2
a = dfs(1, 1) = 1
(if egg breaks at floor 2)b = dfs(4, 2)
(if egg doesn't break: check floors 3-6)
Computing dfs(4, 2):
- Through similar binary search process: optimal is floor 2
- Result:
max(dfs(1, 1), dfs(2, 2)) + 1 = max(1, 2) + 1 = 3
Back to Main:
a = 1
,b = 3
- Since
a ≤ b
, setl = 2
Third Binary Search Iteration:
l = 2, r = 3
mid = (2 + 3 + 1) / 2 = 3
a = dfs(2, 1) = 2
b = dfs(3, 2) = 2
- Since
a ≤ b
, setl = 3
- Now
l = r = 3
, binary search ends
Step 3: Final Result
- Optimal floor to drop from is 3
- Result:
max(dfs(2, 1), dfs(3, 2)) + 1 = max(2, 2) + 1 = 3
Answer: 3 moves minimum in the worst case
Verification of Strategy: The optimal strategy is:
- First drop from floor 3
- If breaks: check floors 1-2 with 1 egg (max 2 more drops)
- If doesn't break: check floors 4-6 with 2 eggs (max 2 more drops)
- Total worst case: 1 + 2 = 3 drops
This demonstrates how the binary search finds the floor (3) where the two worst-case scenarios are balanced (both requiring 2 additional drops), minimizing the overall worst case.
Solution Implementation
1class Solution:
2 def superEggDrop(self, k: int, n: int) -> int:
3 from functools import cache
4
5 @cache
6 def dp(floors: int, eggs: int) -> int:
7 """
8 Calculate minimum number of moves needed in worst case
9 to find critical floor with given number of floors and eggs.
10
11 Args:
12 floors: Number of floors to check
13 eggs: Number of eggs available
14
15 Returns:
16 Minimum number of moves needed in worst case
17 """
18 # Base case: No floors to check
19 if floors < 1:
20 return 0
21
22 # Base case: Only 1 egg left, must check linearly from bottom
23 if eggs == 1:
24 return floors
25
26 # Binary search to find optimal floor to drop egg from
27 # We're looking for the floor that minimizes the worst case
28 left, right = 1, floors
29
30 while left < right:
31 # Calculate middle point (ceiling division)
32 mid = (left + right + 1) >> 1
33
34 # Case 1: Egg breaks at floor mid
35 # Check floors below with one less egg
36 egg_breaks = dp(mid - 1, eggs - 1)
37
38 # Case 2: Egg doesn't break at floor mid
39 # Check floors above with same number of eggs
40 egg_survives = dp(floors - mid, eggs)
41
42 # Binary search to find the floor where the two cases balance
43 # This minimizes the maximum of the two cases
44 if egg_breaks <= egg_survives:
45 left = mid
46 else:
47 right = mid - 1
48
49 # Return worst case scenario (max of two cases) plus current move
50 return max(dp(left - 1, eggs - 1), dp(floors - left, eggs)) + 1
51
52 # Start with n floors and k eggs
53 return dp(n, k)
54
1class Solution {
2 // Memoization table: memo[floors][eggs] stores minimum moves needed
3 private int[][] memo;
4
5 /**
6 * Finds the minimum number of moves required in worst case to find critical floor
7 * @param k - number of eggs available
8 * @param n - number of floors to test
9 * @return minimum number of moves in worst case
10 */
11 public int superEggDrop(int k, int n) {
12 memo = new int[n + 1][k + 1];
13 return findMinMoves(n, k);
14 }
15
16 /**
17 * Recursive helper function with memoization to find minimum moves
18 * @param floors - number of floors remaining to test
19 * @param eggs - number of eggs remaining
20 * @return minimum number of moves needed in worst case
21 */
22 private int findMinMoves(int floors, int eggs) {
23 // Base case: no floors to test
24 if (floors < 1) {
25 return 0;
26 }
27
28 // Base case: only one egg left, must test linearly from bottom
29 if (eggs == 1) {
30 return floors;
31 }
32
33 // Return memoized result if already computed
34 if (memo[floors][eggs] != 0) {
35 return memo[floors][eggs];
36 }
37
38 // Binary search to find optimal floor to drop egg from
39 int left = 1;
40 int right = floors;
41
42 while (left < right) {
43 int mid = (left + right + 1) >> 1; // Equivalent to (left + right + 1) / 2
44
45 // Calculate worst case moves for both scenarios
46 int eggBreaks = findMinMoves(mid - 1, eggs - 1); // Egg breaks: check floors below
47 int eggSurvives = findMinMoves(floors - mid, eggs); // Egg survives: check floors above
48
49 // Binary search optimization: find floor where both cases are balanced
50 if (eggBreaks <= eggSurvives) {
51 left = mid; // Move search to higher floors
52 } else {
53 right = mid - 1; // Move search to lower floors
54 }
55 }
56
57 // Calculate minimum moves for optimal floor (left)
58 // Take maximum of two cases (worst case) and add 1 for current move
59 memo[floors][eggs] = Math.max(
60 findMinMoves(left - 1, eggs - 1), // Egg breaks case
61 findMinMoves(floors - left, eggs) // Egg survives case
62 ) + 1;
63
64 return memo[floors][eggs];
65 }
66}
67
1class Solution {
2public:
3 int superEggDrop(int k, int n) {
4 // dp[floors][eggs] = minimum number of moves needed in worst case
5 // to determine critical floor with 'eggs' eggs and 'floors' floors
6 int dp[n + 1][k + 1];
7 memset(dp, 0, sizeof(dp));
8
9 // Recursive function with memoization to find minimum moves
10 // Parameters: numFloors - number of floors to check
11 // numEggs - number of eggs available
12 // Returns: minimum number of moves needed in worst case
13 auto findMinMoves = [&](this auto&& findMinMoves, int numFloors, int numEggs) -> int {
14 // Base case: no floors to check
15 if (numFloors < 1) {
16 return 0;
17 }
18
19 // Base case: only one egg left, must check linearly from bottom
20 if (numEggs == 1) {
21 return numFloors;
22 }
23
24 // Return memoized result if already computed
25 if (dp[numFloors][numEggs]) {
26 return dp[numFloors][numEggs];
27 }
28
29 // Binary search to find optimal floor to drop egg from
30 // We want to minimize the maximum of two scenarios
31 int left = 1, right = numFloors;
32 while (left < right) {
33 int mid = (left + right + 1) >> 1;
34
35 // Scenario 1: egg breaks at floor 'mid'
36 // Check floors below with one less egg
37 int eggBreaks = findMinMoves(mid - 1, numEggs - 1);
38
39 // Scenario 2: egg doesn't break at floor 'mid'
40 // Check floors above with same number of eggs
41 int eggSurvives = findMinMoves(numFloors - mid, numEggs);
42
43 // Binary search to find the floor where the two scenarios
44 // are most balanced (minimizing the maximum)
45 if (eggBreaks <= eggSurvives) {
46 left = mid;
47 } else {
48 right = mid - 1;
49 }
50 }
51
52 // Store and return the result: maximum of two scenarios + 1 (current move)
53 // We take the maximum because we need to prepare for worst case
54 return dp[numFloors][numEggs] = max(findMinMoves(left - 1, numEggs - 1),
55 findMinMoves(numFloors - left, numEggs)) + 1;
56 };
57
58 return findMinMoves(n, k);
59 }
60};
61
1/**
2 * Determines the minimum number of moves to find the critical floor
3 * with k eggs and n floors using dynamic programming with binary search optimization
4 * @param k - number of eggs available
5 * @param n - number of floors to test
6 * @returns minimum number of moves in worst case
7 */
8function superEggDrop(k: number, n: number): number {
9 // Memoization table: memo[floors][eggs] stores minimum moves needed
10 const memo: number[][] = new Array(n + 1)
11 .fill(0)
12 .map(() => new Array(k + 1).fill(0));
13
14 /**
15 * Recursive function to find minimum moves for given floors and eggs
16 * @param floors - number of floors to check
17 * @param eggs - number of eggs available
18 * @returns minimum number of moves needed in worst case
19 */
20 const findMinMoves = (floors: number, eggs: number): number => {
21 // Base case: no floors to check
22 if (floors < 1) {
23 return 0;
24 }
25
26 // Base case: only one egg, must check linearly from bottom
27 if (eggs === 1) {
28 return floors;
29 }
30
31 // Return memoized result if already computed
32 if (memo[floors][eggs] !== 0) {
33 return memo[floors][eggs];
34 }
35
36 // Binary search to find optimal floor to drop egg from
37 let left = 1;
38 let right = floors;
39
40 while (left < right) {
41 const mid = Math.floor((left + right + 1) / 2);
42
43 // Case 1: egg breaks, check floors below with one less egg
44 const eggBreaks = findMinMoves(mid - 1, eggs - 1);
45
46 // Case 2: egg doesn't break, check floors above with same eggs
47 const eggSurvives = findMinMoves(floors - mid, eggs);
48
49 // Binary search: find the floor where worst cases are balanced
50 if (eggBreaks <= eggSurvives) {
51 left = mid;
52 } else {
53 right = mid - 1;
54 }
55 }
56
57 // Calculate minimum moves: max of two cases (worst case) + 1 for current move
58 const result = Math.max(
59 findMinMoves(left - 1, eggs - 1), // egg breaks at optimal floor
60 findMinMoves(floors - left, eggs) // egg survives at optimal floor
61 ) + 1;
62
63 // Store result in memoization table and return
64 memo[floors][eggs] = result;
65 return result;
66 };
67
68 // Start the recursive search with n floors and k eggs
69 return findMinMoves(n, k);
70}
71
Time and Space Complexity
Time Complexity: O(k * n * log n)
The analysis breaks down as follows:
- The recursive function
dfs(i, j)
has at mostn * k
unique states sincei
ranges from 0 ton
andj
ranges from 0 tok
- Due to memoization with
@cache
, each state is computed only once - For each state
(i, j)
, the function performs a binary search from 1 toi
, which takesO(log i)
iterations, wherei ≤ n
- In each iteration of the binary search, we make recursive calls to
dfs(mid - 1, j - 1)
anddfs(i - mid, j)
, but these are memoized lookups withO(1)
access time after initial computation - Therefore, the total time complexity is
O(k * n * log n)
Space Complexity: O(k * n)
The space complexity consists of:
- The memoization cache stores at most
O(k * n)
entries (one for each unique state) - The recursion stack depth is at most
O(k)
in the worst case, as we decreasej
by 1 in each recursive level until reaching the base case wherej = 1
- Since
O(k * n) > O(k)
for all valid inputs, the overall space complexity isO(k * n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Boundary Logic
One of the most common mistakes is implementing the binary search incorrectly when finding the optimal floor to drop from. The binary search is looking for a specific crossover point where the two scenarios (egg breaks vs. egg doesn't break) are most balanced.
Pitfall Example:
# WRONG: Using standard binary search template without considering the specific condition while left <= right: # Wrong condition mid = (left + right) // 2 # May miss the optimal point egg_breaks = dp(mid - 1, eggs - 1) egg_survives = dp(floors - mid, eggs) if egg_breaks < egg_survives: # Wrong: using < instead of <= left = mid + 1 else: right = mid - 1
Why it's wrong: The standard binary search template doesn't work here because we're not looking for an exact value match. We're searching for the floor where dp(floor - 1, eggs - 1) <= dp(floors - floor, eggs)
transitions to dp(floor, eggs - 1) > dp(floors - floor - 1, eggs)
.
Solution:
# CORRECT: Use ceiling division and proper boundary conditions while left < right: # Stop when left == right mid = (left + right + 1) >> 1 # Ceiling division to avoid infinite loop egg_breaks = dp(mid - 1, eggs - 1) egg_survives = dp(floors - mid, eggs) if egg_breaks <= egg_survives: # Use <= to find the rightmost valid position left = mid # Don't skip mid else: right = mid - 1
2. Off-by-One Errors in Recursive Calls
Another frequent mistake involves incorrect floor calculations when making recursive calls, especially after an egg breaks or doesn't break.
Pitfall Example:
# WRONG: Incorrect floor count calculations egg_breaks = dp(mid, eggs - 1) # Should be mid - 1 egg_survives = dp(floors - mid + 1, eggs) # Should be floors - mid
Why it's wrong:
- If we drop from floor
mid
and the egg breaks, we know the critical floor is in[1, mid-1]
, not[1, mid]
- If the egg doesn't break at floor
mid
, we need to check floors[mid+1, floors]
, which isfloors - mid
floors, notfloors - mid + 1
Solution:
# CORRECT: Accurate floor calculations egg_breaks = dp(mid - 1, eggs - 1) # Check floors 1 to mid-1 egg_survives = dp(floors - mid, eggs) # Check floors mid+1 to floors
3. Forgetting to Add 1 for the Current Move
A subtle but critical error is forgetting to account for the current drop when calculating the total number of moves.
Pitfall Example:
# WRONG: Forgetting to add the current move
return max(dp(left - 1, eggs - 1), dp(floors - left, eggs))
Solution:
# CORRECT: Add 1 for the current drop
return max(dp(left - 1, eggs - 1), dp(floors - left, eggs)) + 1
4. Inefficient Linear Search Instead of Binary Search
Some implementations might use a linear search to find the optimal floor, which significantly increases time complexity.
Pitfall Example:
# WRONG: Linear search through all possible floors
min_moves = float('inf')
for floor in range(1, floors + 1):
egg_breaks = dp(floor - 1, eggs - 1)
egg_survives = dp(floors - floor, eggs)
worst_case = max(egg_breaks, egg_survives) + 1
min_moves = min(min_moves, worst_case)
return min_moves
Why it's wrong: This approach has O(n) complexity per state instead of O(log n), making the overall complexity O(k * n²) instead of O(k * n * log n).
Solution: Use the binary search approach as shown in the correct implementation to efficiently find the optimal floor in O(log n) time.
Which of these properties could exist for a graph but not a tree?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!