1. 程式人生 > 實用技巧 >[LeetCode] 887. Super Egg Drop 超級雞蛋掉落

[LeetCode] 887. Super Egg Drop 超級雞蛋掉落

題目是這樣:你面前有一棟從 1 到NN層的樓,然後給你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 };