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



You are given K eggs, and you have access to a building with N floors from 1 to Nhtml

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 Fwill break, and any egg dropped at or below floor Fwill 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. 1 <= K <= 100
  2. 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]):

  • 雞蛋碎掉:接下來就要用 i-1 個雞蛋來測 k-1 層,因此須要 dp[i-1][k-1] 次操做。
  • 雞蛋沒碎:接下來還能夠用i個雞蛋來測 j-k 層,因此須要 dp[i][j-k] 次操做。
    由於咱們每次都要面對最壞的狀況,因此在第j層扔,須要 max(dp[i-1][k-1], dp[i][j-k])+1 步,狀態轉移方程爲:

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個雞蛋。此時仍是有兩種狀況:

  • 雞蛋碎掉:說明至少能夠測到的不會碎的層數就是 dp[i-1][j-1]。
  • 雞蛋沒碎:那這個雞蛋能夠繼續利用,此時咱們還能夠再向上查找 dp[i-1][j] 層。

那麼加上當前層,總共能夠經過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)

https://leetcode.com/problems/super-egg-drop/discuss/181702/Clear-C%2B%2B-codeRuntime-0-msO(1)-spacewith-explation.No-DPWhat-we-need-is-mathematical-thought!



LeetCode All in One 題目講解彙總(持續更新中...)

相關文章
相關標籤/搜索