Q:你將獲得 K 個雞蛋,並可以使用一棟從 1 到 N 共有 N 層樓的建築。
每個蛋的功能都是一樣的,如果一個蛋碎了,你就不能再把它掉下去。
你知道存在樓層 F ,滿足 0 <= F <= N 任何從高於 F 的樓層落下的雞蛋都會碎,從 F 樓層或比它低的樓層落下的雞蛋都不會破。
每次移動,你可以取一個雞蛋(如果你有完整的雞蛋)並把它從任一樓層 X 扔下(滿足 1 <= X <= N)。
你的目標是確切地知道 F 的值是多少。
無論 F 的初始值如何,你確定 F 的值的最小移動次數是多少?
示例 1:
輸入:K = 1, N = 2
輸出:2
解釋:
雞蛋從 1 樓掉落。如果它碎了,我們肯定知道 F = 0 。
否則,雞蛋從 2 樓掉落。如果它碎了,我們肯定知道 F = 1 。
如果它沒碎,那么我們肯定知道 F = 2 。
因此,在最壞的情況下我們需要移動 2 次以確定 F 是多少。
示例 2:
輸入:K = 2, N = 6
輸出:3
示例 3:
輸入:K = 3, N = 14
輸出:4
提示:
1 <= K <= 100
1 <= N <= 10000
A:
大家可能會想到用二分法來解決,那么我們第一個雞蛋在100/2=50層的位置扔下。假如碎了,那么第二個雞蛋必須只能從第1層一直試到49層,在最壞情況下(答案為49層時),小明共要嘗試50次才能得到答案;假如沒碎,那么可以用第一個雞蛋接着在第75層嘗試,循環往復直到試出答案。於是可知,用二分法,最壞情況為50次。
那么用三分法呢?第一個雞蛋扔在33層,最壞情況是(答案為32層時),小明需要1+32=33次才能得到答案。如果是分成10份呢?第一個雞蛋扔在10層,然后20層…知道100層,最壞情況(答案為99層時),小明共需要扔10(第一個雞蛋)+9(第二個雞蛋)=19次
當然也不是分的越細越好,比如說先分成100/5=20份的話。第一次在5層扔,第二次在10層扔,…,一直扔到95層還沒碎,那么就已經試了19次了。
我們設 f(k,n) 為測試n層樓,當有k個雞蛋時,最壞情況下,最少需要測試的次數。
還是可以按照上面的方法來分析,對於k個雞蛋中的第1個雞蛋來說,小明在第 i 層扔了下去,於是相同的兩種情況會出現:
- 碎了,小明還剩下k-1個雞蛋,i-1 層待測試樓層,於是問題變為 f(k-1, i-1)
- 沒碎,小明還剩下k個雞蛋,n-i 層待測試樓層,於是問題變為 f(k, n-i)
所以同理可得:
之所以里面還有一層循環min,是因為要在當前情況里所有樓層都試一遍,從任一樓層開始里面選擇最小的一種情況。
public static int superEggDrop(int K, int N) {
int[][] dp = new int[K + 1][N + 1];
for (int i = 0; i <= K; i++) {
for (int j = 0; j <= N; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
} else if (i == 1) {
dp[i][j] = j;
} else
dp[i][j] = Integer.MAX_VALUE;
}
}
for (int k = 2; k <= K; k++) {
for (int n = 1; n <= N; n++) {
for (int i = 1; i <= n; i++) {//在當前情況里所有樓層都試一遍
dp[k][n] = Math.min(dp[k][n], 1 + Math.max(dp[k - 1][i - 1], dp[k][n - i]));
}
}
}
return dp[K][N];
}
然后這種方法尼瑪超時了?!!!!畢竟是O(K*N*N)的時間復雜度……
啊……無語問蒼天……java好慘,只能用二分+動態規划???
這個我借用一下別人的python代碼:
/**
* @param {number} K
* @param {number} N
* @return {number}
*/
let dp = undefined
var superEggDrop = function(K, N) {
dp = new Array(K + 1)
for(let i = 0 ;i <= K; i++) dp[i] = new Array(N + 1).fill(-1)
return dpf(K, N)
};
// 剩余k個雞蛋、n層樓需要遍歷
function dpf(k, n) {
if(dp[k][n]!== -1) return dp[k][n]
if(n === 0) return 0
if(k === 1) return n
let ans = Infinity
let left = 1
let right = n
/*
注意, 這里的二分法不是二分法拋雞蛋,每次拋雞蛋實際還是暴力循環遍歷。
這里能用二分法,是因為dpf(k - 1, i - 1)和dpf(k, n - i)一個單調遞增, 一個單調遞減
最大最小問題其實就是要找兩者相交的那個點, 所以可以用二分法
*/
while(left <= right) {
const mid = Math.floor((left + right) / 2)
const broken = dpf(k - 1, mid - 1)
const not_broken = dpf(k, n - mid)
if(broken > not_broken) {
right = mid - 1
ans = Math.min(ans, broken + 1)
} else if(broken < not_broken){
left = mid + 1
ans = Math.min(ans, not_broken + 1)
} else {
ans = Math.min(ans, broken + 1)
break
}
}
dp[k][n] = ans
return ans
}
實際上這個題還有種進階解法。
我們反向考慮一下,設確定當前的雞蛋個數和最多允許的扔雞蛋次數,就知道能夠確定 F 的最⾼樓層數
dp[k][m] = n
當前有 k 個雞蛋,可以嘗試扔 m 次雞蛋
這個狀態下,最壞情況下最多能確切測試⼀棟 n 層的樓
⽐如說 dp[1][7] = 7 表⽰:現在有 1 個雞蛋,允許你扔 7 次;這個狀態下最多給你 7 層樓,使得你可以確定樓層 F 使得雞蛋恰好摔不碎(⼀層⼀層線性探查嘛)
基於下⾯兩個事實:
1.⽆論你在哪層樓扔雞蛋,雞蛋只可能摔碎或者沒摔碎,碎了的話就測樓下,沒碎的話就測樓上。
2.⽆論你上樓還是下樓,總的樓層數 = 樓上的樓層數 + 樓下的樓層數 +1(當前這層樓)。
根據這個特點,可以寫出下⾯的狀態轉移⽅程:
dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1
dp[k][m - 1] 就是樓上的樓層數,因為雞蛋個數 k 不變,也就是雞蛋沒碎,扔雞蛋次數 m 減⼀;
dp[k - 1][m - 1] 就是樓下的樓層數,因為雞蛋個數 k 減⼀,也就是雞蛋碎了,同時扔雞蛋次數 m 減⼀。
PS:這個 m 為什么要減⼀⽽不是加⼀?之前定義得很清楚,這個 m 是⼀個允許的次數上界,⽽不是扔了⼏次。
public static int superEggDrop(int K, int N) {
if (N < 2 || K == 1)
return N;
int[][] dp = new int[K + 1][N + 1];
int m = 0;
//最壞情況下最多能測試 N 層樓
while (dp[K][m] < N) {
m++;
for (int k = 1; k <= K; k++) {
dp[k][m] = dp[k - 1][m - 1] + dp[k][m - 1] + 1;
}
}
return m;
}