LeetCode(887):雞蛋掉落 Super Egg Drop(Java)

2019.11.27 LeetCode 從零單刷個人筆記整理(持續更新)

github:https://github.com/ChopinXBP/LeetCode-Babel

這是一道經典的智力題。由於有雞蛋摔碎的限定條件,因此不能依靠單純的二分查找來解決,可以從遞歸開始,不斷優化和升級方法。

1.遞歸(超時)

在手握K個雞蛋時從第i層樓[0,N]向下丟雞蛋,會有兩種可能:

1.雞蛋摔碎,將剩餘K-1個雞蛋移動到樓[0,i-1]進行測遞歸測試。

2.雞蛋沒碎,將剩餘K個雞蛋移動到樓[i,N]進行測試,可將i層看成0層,對[0,N-i]進行遞歸測試。

遞歸公式

result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1)

2.動態規劃:二分搜索

觀察遞歸公式

result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1)

其中i在[1.N]變化。

而fun1=superEggDrop(K - 1, i - 1)隨i單調增,fun2=superEggDrop(K, N - i)隨i單調減,兩條單調函數的交點即爲兩者最大值的最小取值點。

因此可以對i在[1,N]上進行二分查找。在二分查找過程中,可以設計一個哈希表避免重複計算。

3.動態規劃:自底向上

觀察轉爲動態規劃的遞歸公式

dp[i][j] = min(max(dp[i-1][loc-1], dp[i][j-loc]) + 1)

其中i在[1.j]變化,j在[1, N]變化。

而fun1=dp[i-1][loc-1]隨loc單調增,fun2=dp[i][j-loc]隨loc單調減,兩條單調函數的交點即爲兩者最大值的最小取值點。

尋找loc的思路轉換爲while循環遍歷查找。

令left爲loc-1時的函數值,right爲loc時的函數值,循環遍歷找到最小值的拐點。由於後序j值上限不斷增大,loc拐點值只可能逐漸向右移動,以此可以簡化後序查找。

3.動態規劃(超時):求能夠測得的最少樓層

dp[i][j]代表有i個雞蛋和j次移動時一定能夠測得的最少樓層數。沒有雞蛋時或沒有移動次數時無法測試dp=0;只有1個雞蛋但有j次移動,dp=j(從0層開始逐層測試)。

有i個雞蛋和j次移動時,前j-1次移動的測試結果可能由多種測試路徑得來,將其劃分爲k次和(j-1)-k次遍歷取最大。第j-1次移動雞蛋碎的結果爲dp[i-1][k],雞蛋未碎的結果爲dp[i][(j - 1) - k],當前樓層第j次移動的最少測試結果爲1(摔碎)。

dp[i][j] = max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1

4.動態規劃(次優):求能夠測得的最多樓層

dp[i][j]代表i個雞蛋在j次移動內能測試確定的最多樓層數。

從第一層樓開始更新dp數組。0次移動無法進行測試,初始值dp[i][0]=0。

每多一次移動,可以多進行一次測試。在手中有i-1個雞蛋的第j-1次移動時已經測出樓高dp[i-1][j-1],一次移動最差情況也可以測出當前樓層的情況,因此逆推出當前能夠保證第j次移動得出結果的測試樓層爲dp[i-1][j-1]+1。

第j次移動雞蛋摔碎,可以確定當前層(1層)的結果(碎),剩餘i-1個雞蛋和j-1次移動能得出dp[i-1][j-1]層。

h1 = dp[i][j] = dp[i-1][j-1] + 1;

第j次移動雞蛋未碎,可以確定前dp[i-1][j-1]+1層的結果(不碎),剩餘i個雞蛋和j-1次移動能得出dp[i][j-1]層。

h2 = dp[i][j] = dp[i-1][j-1] + 1 + dp[i-1][j];

綜合結果:

dp = h2 >= max(h1, h2)

5.數學法(最優)

最差情況下(一個雞蛋)用N次移動可以測得N層樓高,因此可以移動次數x必在[1,N]之間,可以對x進行二分查找。

遞歸函數fun(x)代表x次移動能夠測試的最大樓高。

求解思想鏈接

函數推導鏈接


傳送門:雞蛋掉落

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

Each egg is identical in function, and if an egg breaks, you cannot drop it again.

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.

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.

What is the minimum number of moves that you need to know with certainty what F is, regardless of the initial value of F?

你將獲得 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


import java.util.HashMap;

/**
 *
 * You are given K eggs, and you have access to a building with N floors from 1 to N.
 * Each egg is identical in function, and if an egg breaks, you cannot drop it again.
 * 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.
 * 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.
 * What is the minimum number of moves that you need to know with certainty what F is, regardless of the initial value of F?
 * 你將獲得 K 個雞蛋,並可以使用一棟從 1 到 N  共有 N 層樓的建築。
 * 每個蛋的功能都是一樣的,如果一個蛋碎了,你就不能再把它掉下去。
 * 你知道存在樓層 F ,滿足 0 <= F <= N 任何從高於 F 的樓層落下的雞蛋都會碎,從 F 樓層或比它低的樓層落下的雞蛋都不會破。
 * 每次移動,你可以取一個雞蛋(如果你有完整的雞蛋)並把它從任一樓層 X 扔下(滿足 1 <= X <= N)。
 * 你的目標是確切地知道 F 的值是多少。
 * 無論 F 的初始值如何,你確定 F 的值的最小移動次數是多少?
 *
 */

public class SuperEggDrop {

    //遞歸(超時)
    public int superEggDrop2(int K, int N) {
        if(N < 2 || K == 1){
            return N;
        }
        int result = N;
        //在手握K個雞蛋時從第i層樓[0,N]向下丟雞蛋,會有兩種可能:
        //1.雞蛋摔碎,將剩餘K-1個雞蛋移動到樓[0,i-1]進行測遞歸測試。
        //2.雞蛋沒碎,將剩餘K個雞蛋移動到樓[i,N]進行測試,可將i層看成0層,對[0,N-i]進行遞歸測試。
        for(int i = 1; i <= N; i++){
            int curMin = Math.max(superEggDrop2(K - 1, i - 1), superEggDrop2(K, N - i)) + 1;
            result = curMin < result ? curMin : result;
        }
        return result;
    }

    //動態規劃:二分搜索
    //觀察遞歸公式result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1),其中i在[1.N]變化
    //而fun1=superEggDrop(K - 1, i - 1)隨i單調增,fun2=superEggDrop(K, N - i)隨i單調減,兩條單調函數的交點即爲兩者最大值的最小取值點
    //因此可以對i在[1,N]上進行二分查找
    HashMap<Integer, Integer> map = new HashMap<>();
    public int superEggDrop3(int K, int N) {
        if (N == 0){
            return 0;
        }
        if (K == 1){
            return N;
        }
        //因爲K<=100,可以設計一個key值的取值方法,將已經計算過的K,N組合存入哈希表避免重複計算
        int key = N * 1000 + K;
        if (map.containsKey(key)){
            return map.get(key);
        }

        int begin = 1;
        int end = N;
        while (begin + 1 < end) {
            int mid = (begin + end) >> 1;
            int lowVal = superEggDrop(K - 1, mid - 1);
            int highVal = superEggDrop(K, N - mid);

            if (lowVal < highVal){
                begin = mid;
            }
            else if (lowVal > highVal){
                end = mid;
            }
            else{
                end = mid;
                begin = mid;
            }

        }
        int minimum = 1 + Math.min(Math.max(superEggDrop(K - 1, begin - 1), superEggDrop(K, N - begin)),
                Math.max(superEggDrop(K - 1, end - 1), superEggDrop(K, N - end)));
        map.put(key, minimum);
        return minimum;
    }

    //動態規劃:自底向上
    //觀察轉爲動態規劃的遞歸公式dp[i][j] = min(max(dp[i-1][loc-1], dp[i][j-loc]) + 1),其中i在[1.j]變化,j在[1, N]變化
    //而fun1=dp[i-1][loc-1]隨loc單調增,fun2=dp[i][j-loc]隨loc單調減,兩條單調函數的交點即爲兩者最大值的最小取值點
    //尋找loc的思路轉換爲while循環遍歷查找
    public int superEggDrop4(int K, int N) {
        int[][] dp = new int[K + 1][N + 1];
        for(int i = 1; i <= K; i++){
            int loc = 1;
            for(int j = 1; j <= N; j++){
                if(i == 1){
                    dp[i][j] = j;
                    continue;
                }
                //令left爲loc-1時的函數值,right爲loc時的函數值,循環遍歷找到最小值的拐點
                //由於後序j值上限不斷增大,loc拐點值只可能逐漸向右移動,以此可以簡化後序查找
                while (loc < j){
                    int left = Math.max(dp[i - 1][loc - 1], dp[i][j - loc]);
                    int right = Math.max(dp[i - 1][loc], dp[i][j - loc - 1]);
                    if(left <= right){
                        break;
                    }
                    loc++;
                }
                dp[i][j] = Math.max(dp[i - 1][loc - 1], dp[i][j - loc]) + 1;
            }
        }
        return dp[K][N];
    }

    //動態規劃(超時):求能夠測得的最少樓層
    public int superEggDrop5(int K, int N) {
        //dp[i][j]代表有i個雞蛋和j次移動時一定能夠測得的最少樓層數
        int[][] dp = new int[K + 1][N + 1];
        //沒有雞蛋時或沒有移動次數時無法測試dp=0;只有1個雞蛋但有j次移動,dp=j(從0層開始逐層測試)
        for (int i = 1; i <= N; i++) {
            dp[1][i] = i;
            dp[0][i] = 0;
        }
        for (int i = 1; i <= K; i++) {
            dp[i][0] = 0;
        }

        for (int i = 2; i <= K; i++) {
            for (int j = 1; j <= N; j++) {
                //有i個雞蛋和j次移動時,前j-1次移動的測試結果可能由多種測試路徑得來,將其劃分爲k次和(j-1)-k次遍歷取最大
                //第j-1次移動雞蛋碎的結果爲dp[i-1][k],雞蛋未碎的結果爲dp[i][(j - 1) - k],當前樓層第j次移動的最少測試結果爲1(摔碎)
                //dp[i][j] = max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1
                int tMinDrop = N * N;
                for (int k = 0; k < j; k++) {
                    tMinDrop = Math.min(tMinDrop, Math.max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1);
                }
                dp[i][j] = tMinDrop;
            }
        }

        return dp[K][N];
    }

    //動態規劃(次優):求能夠測得的最多樓層
    public int superEggDrop6(int K, int N) {
        //dp[i][j]代表i個雞蛋在j次移動內能測試確定的最多樓層數
        int[][] dp = new int[N + 1][K + 1];
        //從第一層樓開始更新dp數組
        for(int i = 1; i <= N; i++){
            //0次移動無法進行測試,初始值dp[i][0]=0
            dp[i][0] = 0;
            for(int j = 1; j <= K; j++){
                //每多一次移動,可以多進行一次測試。在手中有i-1個雞蛋的第j-1次移動時已經測出樓高dp[i-1][j-1],
                //一次移動最差情況也可以測出當前樓層的情況,因此逆推出當前能夠保證第j次移動得出結果的測試樓層爲dp[i-1][j-1]+1
                //第j次移動雞蛋摔碎,可以確定當前層(1層)的結果(碎),剩餘i-1個雞蛋和j-1次移動能得出dp[i-1][j-1]層
                //h1 = dp[i][j] = dp[i-1][j-1] + 1;
                //第j次移動雞蛋未碎,可以確定前dp[i-1][j-1]+1層的結果(不碎),剩餘i個雞蛋和j-1次移動能得出dp[i][j-1]層
                //h2 = dp[i][j] = dp[i-1][j-1] + 1 + dp[i-1][j];
                //dp = h2 >= max(h1, h2)
                dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + 1;
                if(dp[i][j] >= N){
                    return i;
                }
            }
        }
        return N;
    }

    //數學(最優)
    //最差情況下(一個雞蛋)用N次移動可以測得N層樓高,因此可以移動次數x必在[1,N]之間,可以對x進行二分查找
    public int superEggDrop(int K, int N) {
        int begin = 1;
        int end = N;
        while(begin < end){
            int mid = (begin + end) >> 1;
            if(fun(mid, K, N) < N){
                begin = mid + 1;
            }else {
                end = mid;
            }
        }
        return begin;
    }

    //遞歸函數fun(x)代表x次移動能夠測試的最大樓高
    //https://leetcode-cn.com/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode/
    //https://leetcode-cn.com/problems/super-egg-drop/solution/shu-xue-fa-jie-shi-by-lycao/
    private int fun(int x, int k, int n){
        int result = 0;
        int r = 1;
        for(int i = 1; i <= k; ++i){
            r *= x - i + 1;
            r /= i;
            result += r;
            if(result >= n){
                break;
            }
        }
        return result;
    }
}




#Coding一小時,Copying一秒鐘。留個言點個讚唄,謝謝你#

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章