zoukankan      html  css  js  c++  java
  • [LeetCode] 887. Super Egg Drop 超级鸡蛋掉落

          题目是这样:你面前有一栋从 1 到 N 共 N 层的楼,然后给你 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、暴力穷举尝试在所有楼层 1 <= i <= N 扔鸡蛋,每次选择最坏情况尝试次数最少的那一层;

    2、每次扔鸡蛋有两种可能,要么碎,要么没碎;

    3、如果鸡蛋碎了,F 应该在第 i 层下面,否则,F 应该在第 i 层上面;

    4、鸡蛋是碎了还是没碎,取决于哪种情况下尝试次数更多,因为我们想求的是最坏情况下的结果

    状态转移的伪代码如下:

     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 };

          算法复杂度分析:

          动态规划算法的时间复杂度就是  子问题个数 × 函数本身的复杂度。

          子问题个数:也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。

          函数本身的复杂度:就是忽略递归部分的复杂度,这里 dp 函数中用了一个二分搜索,

    所以函数本身的复杂度是 O(logN)。

         所以使用了二分搜索优化之后的算法的总时间复杂度是 O(K*N*logN), 空间复杂度 O(KN)。

    效率上比未优化的算法 O(KN^2) 要高效一些。

           

  • 相关阅读:
    get通配符
    常用正则表达式(合)
    2.A star
    1.序
    机器人运动规划04《规划算法》
    机器人运动规划03什么是运动规划
    6.2 性能优化
    6.1 内存机制及使用优化
    5.9 热修复技术
    5.8 反射机制
  • 原文地址:https://www.cnblogs.com/wangxf2019/p/13922359.html
Copyright © 2011-2022 走看看