You are given K
eggs, and you have access to a building with N
floors from 1
to N
. html
Each egg is identical in function, and if an egg breaks, you cannot drop it again.git
You know that there exists a floor F
with 0 <= F <= N
such that any egg dropped at a floor higher than F
will break, and any egg dropped at or below floor F
will not break.github
Each move, you may take an egg (if you have an unbroken one) and drop it from any floor X
(with 1 <= X <= N
). 數組
Your goal is to know with certainty what the value of F
is.less
What is the minimum number of moves that you need to know with certainty what F
is, regardless of the initial value of F
?ide
Example 1:函數
Input: K = 1, N = 2 Output: 2 Explanation: Drop the egg from floor 1. If it breaks, we know with certainty that F = 0. Otherwise, drop the egg from floor 2. If it breaks, we know with certainty that F = 1. If it didn't break, then we know with certainty F = 2. Hence, we needed 2 moves in the worst case to know what F is with certainty.
Example 2:測試
Input: K = 2, N = 6 Output: 3
Example 3:優化
Input: K = 3, N = 14 Output: 4
Note:ui
1 <= K <= 100
1 <= N <= 10000
這道題說給了咱們K個雞蛋,還有一棟共N層的大樓,說是雞蛋有個臨界點的層數F,高於這個層數扔雞蛋就會碎,不然就不會,問咱們找到這個臨界點最小須要多少操做,注意這裏的操做只有當前還有沒碎的雞蛋才能進行。這道題是基於經典的扔雞蛋的問題改編的,原題是有 100 層樓,爲了測雞蛋會碎的臨街點,最少能夠扔幾回?答案是隻用扔 14 次就能夠測出來了,講解能夠參見油管上的這個視頻,這兩道題看着很類似,實際上是有不一樣的。這道題限制了雞蛋的個數K,假設咱們只有1個雞蛋,碎了就不能再用了,這時咱們要測 100 樓的臨界點的時候,只能一層一層去測,當某層雞蛋碎了以後,就知道臨界點了,因此最壞狀況要測 100 次,注意要跟經典題目中扔 14 次要區分出來。那麼假若有兩個雞蛋呢,其實須要的次數跟經典題目中的同樣,都是 14 次,這是爲啥呢?由於在經典題目中,咱們是分別間隔 14,13,12,...,2,1,來扔雞蛋的,當咱們有兩個雞蛋的時候,咱們也能夠這麼扔,第一個雞蛋仍在 14 樓,若碎了,說明臨界點必定在 14 樓之內,能夠用第二個雞蛋去一層一層的測試,因此最多操做 14 次。若第一個雞蛋沒碎,則下一次扔在第 27 樓,假如碎了,說明臨界點在 (14,27] 範圍內,用第二個雞蛋去一層一層測,總次數最多 13 次。若第一個雞蛋還沒碎,則繼續按照 39, 50, ..., 95, 99,等層數去測,總次數也只可能愈來愈少,不會超過 14 次的。可是照這種思路分析的話,博主就不太清楚有3個雞蛋,在 100 樓測,最少的步驟數,答案是9次,博主不太會分析怎麼測的,各位看官大神知道的話必定要告訴博主啊。
其實這道題比較好的解法是用動態規劃 Dynamic Programming,由於這裏有兩個變量,雞蛋數K和樓層數N,因此就要使用一個二維數組 DP,其中 dp[i][j] 表示有i個雞蛋,j層樓要測須要的最小操做數。那麼咱們在任意k層扔雞蛋的時候就有兩種狀況(注意這裏的k跟雞蛋總數K沒有任何關係,k的範圍是 [1, j]):
dp[i][j] = min(dp[i][j], max(dp[i - 1][k - 1], dp[i][j - k]) + 1) ( 1 <= k <= j )
這種寫法會超時 Time Limit Exceeded,代碼請參見評論區1樓,OJ 對時間卡的仍是蠻嚴格的,因此咱們就須要想辦法去優化時間複雜度。這種寫法裏面咱們枚舉了 [1, j] 範圍全部的k值,總時間複雜度爲 O(KN^2),若咱們仔細觀察 dp[i - 1][k - 1] 和 dp[i][j - k],能夠發現前者是隨着k遞增,後者是隨着k遞減,且每次變化的值最多爲1,因此只要存在某個k值使得兩者相等,那麼就能獲得最優解,不然取最相近的兩個k值作比較,因爲這種單調性,咱們能夠在 [1, j] 範圍內對k進行二分查找,找到第一個使得 dp[i - 1][k - 1] 不小於 dp[i][j - k] 的k值,而後用這個k值去更新 dp[i][j] 便可,這樣時間複雜度就減小到了 O(KNlgN),其實也是險過,參見代碼以下:
解法一:
class Solution { public: int superEggDrop(int K, int N) { vector<vector<int>> dp(K + 1, vector<int>(N + 1)); for (int j = 1; j <= N; ++j) dp[1][j] = j; for (int i = 2; i <= K; ++i) { for (int j = 1; j <= N; ++j) { dp[i][j] = j; int left = 1, right = j; while (left < right) { int mid = left + (right - left) / 2; if (dp[i - 1][mid - 1] < dp[i][j - mid]) left = mid + 1; else right = mid; } dp[i][j] = min(dp[i][j], max(dp[i - 1][right - 1], dp[i][j - right]) + 1); } } return dp[K][N]; } };
進一步來想,對於固定的k,dp[i][j-k] 會隨着j的增長而增長,最優決策點也會隨着j單調遞增,因此在每次移動j後,從上一次的最優決策點的位置來繼續向後查找最優勢便可,這樣時間複雜度就優化到了 O(KN),咱們使用一個變量s表示當前的j值下的的最優決策點,而後當j值改變了,咱們用一個 while 循環,來找到第下一個最優決策點s,使得 dp[i - 1][s - 1] 不小於 dp[i][j - s],參見代碼以下:
解法二:
class Solution { public: int superEggDrop(int K, int N) { vector<vector<int>> dp(K + 1, vector<int>(N + 1)); for (int j = 1; j <= N; ++j) dp[1][j] = j; for (int i = 2; i <= K; ++i) { int s = 1; for (int j = 1; j <= N; ++j) { dp[i][j] = j; while (s < j && dp[i - 1][s - 1] < dp[i][j - s]) ++s; dp[i][j] = min(dp[i][j], max(dp[i - 1][s - 1], dp[i][j - s]) + 1); } } return dp[K][N]; } };
其實咱們還能夠進一步優化時間複雜度到 O(KlgN),不過就比較難想到了,須要將問題轉化一下,變成已知雞蛋個數,和操做次數,求最多能測多少層樓的臨界點。仍是使用動態規劃 Dynamic Programming 來作,用一個二維 DP 數組,其中 dp[i][j] 表示當有i次操做,且有j個雞蛋時能測出的最高的樓層數。再來考慮狀態轉移方程如何寫,因爲 dp[i][j] 表示的是在第i次移動且使用第j個雞蛋測試第 dp[i-1][j-1]+1 層,由於上一個狀態是第i-1次移動,且用第j-1個雞蛋。此時仍是有兩種狀況:
那麼加上當前層,總共能夠經過i次操做和j個雞蛋查找的層數範圍是 [0, dp[i-1][j-1] + dp[i-1][j] + 1],這樣就能夠獲得狀態轉移方程以下:
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] + 1
當 dp[i][K] 正好小於N的時候,i就是咱們要求的最小次數了,參見代碼以下:
解法三:
class Solution { public: int superEggDrop(int K, int N) { vector<vector<int>> dp(N + 1, vector<int>(K + 1)); int m = 0; while (dp[m][K] < N) { ++m; for (int j = 1; j <= K; ++j) { dp[m][j] = dp[m - 1][j - 1] + dp[m - 1][j] + 1; } } return m; } };
咱們能夠進一步的優化空間,由於當前的操做次數值的更新只跟上一次操做次數有關,因此咱們並不須要保存全部的次數,可使用一個一維數組,其中 dp[i] 表示當前次數下使用i個雞蛋能夠測出的最高樓層。狀態轉移方程的推導思路仍是跟上面同樣,參見代碼以下:
解法四:
class Solution { public: int superEggDrop(int K, int N) { vector<int> dp(K + 1); int res = 0; for (; dp[K] < N; ++res) { for (int i = K; i > 0; --i) { dp[i] = dp[i] + dp[i - 1] + 1; } } return res; } };
下面這種方法就很是的 tricky 了,竟然推導出了使用k個雞蛋,移動x次所能測的最大樓層數的通項公式,推導過程能夠參見這個帖子,通項公式以下:
f(k,x) = x(x-1)..(x-k)/k! + ... + x(x-1)(x-2)/3! + x(x-1)/2! + x
這數學功底也太好了吧,有了通向公式後,咱們就能夠經過二分搜索法 Binary Search 來快速查找知足題目的x。這裏實際上是博主以前總結貼 LeetCode Binary Search Summary 二分搜索法小結 中的第四類,用子函數看成判斷關係,這裏子函數就是用來實現上面的通向公式的,不過要判斷,當累加和大於等於N的時候,就要把當的累加和返回,這樣至關於進行了剪枝,由於在二分法中只須要知道其跟N的大小關係,並不 care 到底大了多少,這樣快速定位x的方法運行速度貌似比上面的 DP 解法要快很多,可是這通項公式尼瑪誰能容易的推導出來,只能膜拜歎服了,參見代碼以下:
解法五:
class Solution { public: int superEggDrop(int K, int N) { int left = 1, right = N; while (left < right) { int mid = left + (right - left) / 2; if (helper(mid, K, N) < N) left = mid + 1; else right = mid; } return right; } int helper(int x, int K, int N) { int res = 0, r = 1; for (int i = 1; i <= K; ++i) { r *= x - i + 1; r /= i; res += r; if (res >= N) break; } return res; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/887
參考資料:
https://leetcode.com/problems/super-egg-drop/
http://www.javashuo.com/article/p-xlvpiomk-ky.html
https://www.acwing.com/solution/leetcode/content/579/
https://leetcode.com/problems/super-egg-drop/discuss/159508/easy-to-understand
https://leetcode.com/problems/super-egg-drop/discuss/299526/BinarySearch-or-Easiest-or-Explanation
https://leetcode.com/problems/super-egg-drop/discuss/158974/C%2B%2BJavaPython-2D-and-1D-DP-O(KlogN)