[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 题目讲解汇总(持续更新中...)

相关文章
相关标签/搜索