[LeetCode] 887. Super Egg Drop 超級雞蛋掉落
題目是這樣:你面前有一棟從 1 到N
共N
層的樓,然後給你K
個雞蛋(K
至少為 1)。
現在確定這棟樓存在樓層0 <= F <= N
,在這層樓將雞蛋扔下去,雞蛋恰好沒摔碎
(高於F
的樓層都會碎,低於F
的樓層都不會碎)。現在問你,最壞情況下,你至少
要扔幾次雞蛋,才能確定這個樓層F
呢?
注:這裡的樓層數和我們日常生活中理解的有差異,樓層數0表示地面,從地面扔雞蛋
一定不碎,樓層數1,即表示我們日常認知裡的2樓。
題目中 求 最壞情況下,為確定樓層 F,扔雞蛋最少次數。題目給我們的直觀感覺是 使用
二分法求解,思想類似於 “小老鼠喝毒藥”,使用最少的老鼠,找出哪一瓶是毒藥。得到的最少
次數為 logN,這樣,題目給的 引數 K就沒用到,那這種解法就一定有問題。
問題在哪呢?假設,現在 k=1,N=100,按照上面二分法的思路,在50層扔雞蛋,如果雞蛋沒碎,
F 在區間 [51,100 ],此時雞蛋還能繼續用,但是如果雞蛋碎了,即使我們已經知道 F 在區間 [1,49 ],
但是我們仍然無法找到 F 。這時此時唯一的辦法是,從第一樓開始,往上一直到100層,一層一層地
扔雞蛋,直到雞蛋碎在 m 層,此時 扔了m次雞蛋 ,得到 F = m-1。最壞情況下,一直到N=100層雞
蛋才碎了,扔了100次,得到 F = 99。這裡的最壞情況是指雞蛋破碎一定發生在搜尋區間窮盡時,
和求演算法時間複雜度的最壞情況概念很相似。
題目的含義中有 “最壞情況下最小的扔雞蛋次數” ,可以嘗試使用動態規劃的方法求解;
1.定義狀態:「狀態」很明顯,就是當前擁有的雞蛋數K
和需要測試的樓層數N。隨著測試
的進行,雞蛋個數可能減少,樓層的搜尋範圍會減小,這就是狀態的變化。
2.狀態轉移:「選擇」其實就是去選擇哪層樓扔雞蛋。對1到N之間的所有樓層,我們可以
計算在最壞情況下找到 F 需要扔雞蛋的次數n(i)。然後取最小的n(i),即得到我們想要的結果。
狀態轉移的虛擬碼如下:
1 def dp(K, N): 2 for 1 <= i <= N: 3 # 最壞情況下的最少扔雞蛋次數4 res = min(res, 5 max( 6 dp(K - 1, i - 1), # 碎 7 dp(K, N - i) # 沒碎 8 ) + 1 # 在第 i 樓扔了一次 9 ) 10 return res
上面的狀態轉移是 使用線性的方式,使用一個 for loop,算出所有樓層的n(i),最後取最小的。
這種方法在LeetCode上會有超時。
可以使用二分搜尋的方法優化。這裡的二分搜尋和上面提到的不是一回事。虛擬碼如下:
1 lo, hi = 1, N 2 while lo <= hi: 3 mid = (lo + hi) // 2 4 broken = dp(K - 1, mid - 1) # 碎 5 not_broken = dp(K, N - mid) # 沒碎 6 # res = min(max(碎,沒碎) + 1) 7 if broken > not_broken: 8 hi = mid - 1 9 res = min(res, broken + 1) 10 else: 11 lo = mid + 1 12 res = min(res, not_broken + 1) 13 return res
因為遞迴中存在大量的重複子問題,所以我們可以使用備忘錄的方法,避免子問題的重複計算,
提高效率。最終的程式碼如下:
1 //N層樓中扔雞蛋,找到最壞情況下,雞蛋恰好不碎的樓層,所需的最少實驗次數 2 class Solution { 3 public: 4 int superEggDrop(int K, int N) 5 { 6 memo.clear(); 7 return dp(K,N); 8 } 9 private: 10 int dp(int K, int N) 11 { 12 //base case 13 if(K==1) return N; 14 if(N==0) return 0; 15 //檢索備忘錄,若備忘錄中有相應的狀態結果,直接返回 16 if(memo.find(N*100+K)!=memo.end()) return memo[N*100+K]; 17 //結果初始化 18 int res = INT_MAX; 19 //線性搜尋 20 // for(int i=1;i<=N;++i) 21 // { 22 // res = min(res,max(dp(K,N-i),dp(K-1,i-1))+1); 23 // } 24 //二分搜尋 25 int low = 1,high = N; 26 while(low<=high) 27 { 28 int mid = (low+high)/2; 29 int broken = dp(K-1,mid-1);//在mid層扔雞蛋,碎 30 int not_broken = dp(K,N-mid);//在midc層人雞蛋,不碎 31 if(broken>not_broken)//打碎了是最壞情況 32 { 33 high = mid-1;//縮小搜尋區間到[low,mid-1] 34 res = min(res,broken+1); 35 } 36 else //沒打碎是最壞情況 37 { 38 low = mid +1;//縮小搜尋區間 [mid+1,high] 39 res = min(res,not_broken + 1); 40 } 41 } 42 //計算的結果記錄到備忘錄中 43 memo[N*100+K] = res; 44 return res; 45 } 46 unordered_map<int,int> memo;//備忘錄,記錄計算過的狀態 47 };