Topics › Dynamic Programming › MCM DP - Partition DP
Super Egg Drop
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You have k identical eggs and a building with floors 1 to n. There is an unknown critical floor f with 0 ≤ f ≤ n: an egg dropped from any floor above f breaks, and an egg dropped from floor f or below survives. In one move you drop an unbroken egg from a floor of your choice. A broken egg is gone; a surviving egg can be reused. Return the minimum number of moves that guarantees you learn f, no matter what f is.
Constraints (LeetCode 887): 1 ≤ k ≤ 100, 1 ≤ n ≤ 10^4.
“Minimum number of moves” is a worst-case quantity: we choose a strategy, an adversary chooses f, and we want the strategy whose worst case is smallest.
Example 1
k = 1, n = 2
answer = 2 // drop from 1; if it survives, drop from 2. With one egg you cannot skip floors.
Example 2
k = 2, n = 6
answer = 3 // drop from 3: if it breaks, try 1 then 2; if not, try 5, then 4 or 6
The recurrence: what happens after one drop?
Say we have k eggs and n floors still in doubt, and we drop from the x-th of those floors:
- It breaks.
fis belowx. We havek - 1eggs andx - 1floors left to check. - It survives.
fisxor above. We still havekeggs, and then - xfloors abovexremain.
Only the number of floors in doubt matters, not their absolute heights, so the subproblem is again “(eggs, floors)”. We pay one move, and the adversary picks the worse outcome. We pick the best x:
moves(k, 0) = 0, moves(k, 1) = 1, moves(1, n) = n
moves(k, n) = 1 + min over x = 1..n of max( moves(k - 1, x - 1), moves(k, n - x) )
The one-egg case is n because with a single egg you cannot risk skipping a floor: if you jump and it breaks, the floors you skipped are unresolved.
Approach 1: Recursion
Intuition. Try every floor for the next drop and take the best worst case.
Algorithm.
- Handle the base cases above.
- For each
xin1..n, compute1 + max(recursion(k - 1, x - 1), recursion(k, n - x)). - Return the minimum.
static int recursion(int k, int n) {
if (n == 0 || n == 1) return n; // 0 floors: nothing to test; 1 floor: one drop
if (k == 1) return n; // one egg: floor by floor from the bottom
int best = Integer.MAX_VALUE;
for (int x = 1; x <= n; x++) {
int breaks = recursion(k - 1, x - 1); // answer is below x, one egg fewer
int survives = recursion(k, n - x); // answer is above x, same eggs
best = Math.min(best, 1 + Math.max(breaks, survives));
}
return best;
}
Complexity. Exponential time; already k = 2 costs about 2^n calls. Space O(n) for the stack. Only usable for toy sizes.
Approach 2: Memoization
Intuition. The state is (k, n): at most k × n values. Cache them.
Algorithm. Same recursion with a dp[k + 1][n + 1] table initialised to -1.
static int memoization(int k, int n) {
int[][] dp = new int[k + 1][n + 1];
for (int[] row : dp) Arrays.fill(row, -1);
return memo(k, n, dp);
}
static int memo(int k, int n, int[][] dp) {
if (n == 0 || n == 1) return n;
if (k == 1) return n;
if (dp[k][n] != -1) return dp[k][n];
int best = Integer.MAX_VALUE;
for (int x = 1; x <= n; x++) {
best = Math.min(best, 1 + Math.max(memo(k - 1, x - 1, dp), memo(k, n - x, dp)));
}
return dp[k][n] = best;
}
Complexity. Time O(k × n^2): k × n states, each scanning n floors. Space O(k × n). For k = 100, n = 10^4 that is 10^10 steps, far too slow, so we need to speed up the inner loop.
Approach 3: Memoization with binary search
Intuition. Look at the two terms inside the max as functions of x:
breaks(x) = moves(k - 1, x - 1)never decreases asxgrows (more floors below can only need more moves).survives(x) = moves(k, n - x)never increases asxgrows (fewer floors above).
The maximum of a rising curve and a falling curve is smallest where they cross. So instead of scanning all x, binary search for the crossing: if breaks(mid) < survives(mid) the crossing is to the right, if breaks(mid) > survives(mid) it is to the left, and if they are equal we have found it. When the loop ends, the crossing lies between lo and hi, so the best x is one of them.
Algorithm.
- Base cases and cache lookup as before.
- Binary search
xon[1, n]using the comparison above untillo + 1 ≥ hi. - Evaluate both
loandhi; store and return the better one.
static int memoBinarySearch(int k, int n) {
int[][] dp = new int[k + 1][n + 1];
for (int[] row : dp) Arrays.fill(row, -1);
return memoBs(k, n, dp);
}
static int memoBs(int k, int n, int[][] dp) {
if (n == 0 || n == 1) return n;
if (k == 1) return n;
if (dp[k][n] != -1) return dp[k][n];
// breaks(x) = memoBs(k-1, x-1) rises with x, survives(x) = memoBs(k, n-x) falls with x:
// the best x sits where the two curves cross, so binary search for the crossing.
int lo = 1, hi = n;
while (lo + 1 < hi) {
int mid = (lo + hi) >>> 1;
int breaks = memoBs(k - 1, mid - 1, dp);
int survives = memoBs(k, n - mid, dp);
if (breaks < survives) lo = mid;
else if (breaks > survives) hi = mid;
else lo = hi = mid;
}
int best = Integer.MAX_VALUE;
for (int x : new int[]{lo, hi}) {
best = Math.min(best, 1 + Math.max(memoBs(k - 1, x - 1, dp), memoBs(k, n - x, dp)));
}
return dp[k][n] = best;
}
Complexity. Time O(k × n × log n): each state now does O(log n) work. Space O(k × n). This passes LeetCode, but the next idea is both faster and simpler.
Approach 4: Optimal, flip the question
Intuition. Instead of asking “how many moves do k eggs need for n floors?”, ask the inverse: “with m moves and e eggs, how many floors can I fully resolve?” Call that floors(m, e). Make the first drop from floor floors(m - 1, e - 1) + 1:
- If it breaks, the
floors(m - 1, e - 1)floors below can be resolved with the remainingm - 1moves ande - 1eggs. - If it survives, we can resolve
floors(m - 1, e)more floors above it withm - 1moves andeeggs.
Counting the floor we dropped from, that gives
floors(m, e) = floors(m - 1, e - 1) + floors(m - 1, e) + 1, floors(0, e) = floors(m, 0) = 0
and no choice of x is needed: the recurrence directly says where to drop. The answer is the smallest m with floors(m, k) ≥ n. We grow m one move at a time, keeping a single array over eggs. Updating e from high to low ensures floors[e - 1] on the right-hand side is still the value for m - 1 moves.
Algorithm.
floors[0..k] = 0,moves = 0.- While
floors[k] < n: incrementmoves, then forefromkdown to1setfloors[e] = floors[e - 1] + floors[e] + 1. - Return
moves.
static int optimal(int k, int n) {
int[] floors = new int[k + 1]; // floors[e]: floors fully resolved with e eggs and `moves` drops
int moves = 0;
while (floors[k] < n) {
moves++;
for (int e = k; e >= 1; e--) { // descending so floors[e - 1] is still the previous move's value
floors[e] = floors[e - 1] + floors[e] + 1; // floors below (egg broke) + floors above (survived) + this floor
}
}
return moves;
}
Dry run on Example 2 (k = 2, n = 6):
| moves | floors[1] | floors[2] | floors[2] ≥ 6? |
|---|---|---|---|
| 0 | 0 | 0 | no |
| 1 | 1 | 1 | no |
| 2 | 2 | 3 | no |
| 3 | 3 | 6 | yes, answer 3 |
With 3 moves and 2 eggs we drop first from floor floors(2, 1) + 1 = 3, which matches the strategy in the example.
Complexity. Time O(k × answer), and the answer is at most n (and about log2 n when eggs are plentiful). Space O(k). The values never overflow: we stop as soon as floors[k] ≥ n, and floors[e] ≤ floors[k], so every entry stays below 2n + 1.
A neat by-product: floors(m, e) = C(m, 1) + C(m, 2) + ... + C(m, e). With two eggs, m + m(m - 1)/2 ≥ 100 first holds at m = 14, the famous answer to the two-egg, 100-floor puzzle.
Edge cases and pitfalls
- One egg. The answer is
n; you must climb floor by floor. - Many eggs. Once
k ≥ ⌈log2(n + 1)⌉, extra eggs stop helping and the answer is plain binary search,⌈log2(n + 1)⌉, becausefloors(m, e) = 2^m - 1whene ≥ m. The testk = 100,n = 10^4expects 14. fcan be 0. The egg might break even on floor 1. That is whynfloors haven + 1possible answers, and whyfloors(m, e)counts the dropped floor itself with the+ 1.- Memo stack depth.
memo(k, n - 1)recurses one floor at a time, so the plain memoised version reaches depthn. That is fine for10^4in Java, but another reason to prefer approach 4. - Binary search ends. Always evaluate both
loandhiat the end; the crossing can fall between two integers.
Related problems
- Matrix chain multiplication uses the same “try every split point, combine the two sides” structure.
- Palindrome Partitioning (MCM Variation) minimises over split points in the same style.
- Minimum cost to cut the stick is another minimise-over-every-cut interval DP.
- Rod cutting problem tries every first piece of a rod.