[LeetCode] 887、鸡蛋掉落

题目描述

你将获得 K 个鸡蛋,并可以使用一栋从 1N 共有 N 层楼的建筑。每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。你的目标是确切地知道 F 的值是多少。无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。

解题思路

这个题挺难的,面试还爱问。最直接的思路是线性扫描,但是这样效率不高。实际上,如果本题不限制鸡蛋个数的话,二分思路显然可以得到最少尝试的次数,但问题是,现在给你限制了鸡蛋的个数 K,直接使用二分思路就不行了(比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?)。我的实现。

  • 动态规划:前文「不同定义有不同解法」就提过,找动态规划的状态转移本就是见仁见智,比较玄学的事情,不同的状态定义可以衍生出不同的解法,其解法和复杂程度都可能有巨大差异。这里就是一个很好的例子。

    • 如果根据“问什么定义什么”的思路,本题的状态表示应该为:

      # 用带有两个状态参数的 dp 函数来表示状态转移
      def dp(k, n) -> int
      # 当前状态为 k 个鸡蛋,面对 n 层楼
      # 返回这个状态下最少的扔鸡蛋次数
      
      # 或用二维的 dp 数组表示的话也是一样的
      dp[k][n] = m
      # 当前状态为 k 个鸡蛋,面对 n 层楼
      # 这个状态下最少的扔鸡蛋次数为 m
      

      这种定义方式效率比较低,会报超时错误。具体解法和代码请参照大佬题解和我的实现。

    • 重新定义状态转移:(状态表示比较难理解,推荐记一下,这算是个很难的动态规划题了)

      现在,我们稍微修改下 dp 数组的定义,确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就能准确测得的最高楼层数。具体来说是这个意思:

      dp[k][m] = n
      # 当前有 k 个鸡蛋,可以尝试扔 m 次鸡蛋
      # 这个状态下,最坏情况下最多能确切测试一栋 n 层的楼
      
      # 比如说 dp[1][7] = 7 表示:
      # 现在有 1 个鸡蛋,允许你扔 7 次;
      # 这个状态下最多给你 7 层楼,使得你可以确定楼层 F 使得鸡蛋恰好摔不碎 (一层一层线性探查嘛)
      

      这其实就是我们原始思路的一个**「反向」版本**,我们先不管这种思路的状态转移怎么写,先来思考一下这种定义之下,最终想求的答案是什么?

      我们最终要求的其实是扔鸡蛋次数 m,但是这时候 m 在状态之中而不是 dp 数组的结果,可以这样处理:

      int superEggDrop(int K, int N) {
      
          int m = 0;
          while (dp[K][m] < N) {
              m++;
              // 状态转移...
          }
          return m;
      }
      

      题目不是给你 K 个鸡蛋,N 层楼,让你求最坏情况下最少的测试次数 m 吗?while 循环结束的条件是 dp[K][m] == N,也就是给你 K 个鸡蛋,测试 m 次,最坏情况下最多能测试 N 层楼

      下面最重要的是要明确状态转移方程,有下面两个事实:

      1. 无论你在哪层楼扔鸡蛋(所以不需要穷举楼层了),鸡蛋只可能摔碎或者没摔碎(两种可能),碎了的话就测楼下,没碎的话就测楼上
      2. 无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)

      根据这两个特点,可以写出下面的状态转移方程:

      dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1

      dp[k][m - 1] 就是楼上的楼层数,因为鸡蛋没碎,所以鸡蛋个数 k 不变,扔鸡蛋次数 m 减一;

      dp[k - 1][m - 1] 就是楼下的楼层数,因为鸡蛋碎了,所有k-1,同时扔鸡蛋次数 m 减一。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QVpyxJTM-1580207996950)(./imgs/20200128181958.jpg)]

参考代码

解法一(会超时)

class Solution:
    def superEggDrop(self, K: int, N: int) -> int:

        memo = dict()
        def dp(K, N) -> int:
            # base case
            if K == 1: return N
            if N == 0: return 0
            # 避免重复计算
            if (K, N) in memo:
                return memo[(K, N)]

            res = float('INF')
            # 穷举所有可能的选择
            for i in range(1, N + 1):
                res = min(res, 
                        max(
                                dp(K, N - i), 
                                dp(K - 1, i - 1)
                            ) + 1
                    )
            # 记入备忘录
            memo[(K, N)] = res
            return res
        
        return dp(K, N)

解法二(可AC)

class Solution {
public:
    int superEggDrop(int K, int N) {
        // 定义dp[K+1][N+1],m 最多不会超过 N 次(线性扫描)
        vector<vector<int> > dp(K + 1, vector<int>(N + 1, 0));
        // base case:
        // dp[0][..] = 0
        // dp[..][0] = 0
        int m = 0;
        while(dp[K][m] < N){
            m++;
            for(int k = 1; k <= K; k++)
                dp[k][m] = dp[k - 1][m - 1] + dp[k][m - 1] + 1;  // 碎 + 没碎 + 1
        }
        
        return m;
    }
};
发布了436 篇原创文章 · 获赞 626 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/ft_sunshine/article/details/104101604