算法刷题笔记 -- 扔鸡蛋

"博客重新开张,记录一下准备面试期间刷到的,难度较高的算法题"

Posted by Palette on November 11, 2021

LeetCode 887. 鸡蛋掉落

难度:Hard

题目简介

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。 已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。 每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。 请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

思路分析

  1. 首先来整理一下题目内容:我们有k个鸡蛋,并且需要找出该栋楼中的某层f,使得高于f层时扔鸡蛋一定会碎,低于f层时一定不会碎;我们每一次可以拿没有碎的鸡蛋去尝试,求问能够确定f的最小操作次数是多少?
  2. 很直观的想法就是运用动态规划思想解题,定义二维数组drop[i][j],其表示当手上的鸡蛋只有i个时,当前总楼层为j时,咱们能够确定f的最小操作次数即为drop[i][j]。状态转移方程实际上相对比较简单,当我们在楼层x抛下一枚鸡蛋时,假设鸡蛋碎了,那么鸡蛋数量-1,同时我们可以确定此时f层只能够在x层以下;假设鸡蛋没碎,那么我们可以确定f层只能够在x层或者以上:drop[i][j] = 1 + min(max(drop[i-1][j-1], drop[i][n-j])) where 1 <= j <= n
  3. 对于该状态转移方程,首先1代表着当前扔鸡蛋这次操作,然后drop[i-1][j-1]代表着此时扔下鸡蛋破碎之后,可以确定f层肯定是在j层以下,所对应的最小操作次数。drop[i][n-j]代表扔下鸡蛋没碎,可以确定f层肯定是在j层或者以上的最小操作次数,由于我们需要找到最坏情况下的最小操作次数,所以两者必须取最大值max。同时,对于在不同楼层扔下鸡蛋这个操作,我们需要取所得最小值的楼层从而使得最坏情况下的操作次数最少
  4. 如果我们直接暴力转移求解每个状态的 \textit{dp}dp 值,时间复杂度是为 O(kn^2),即一共有 O(kn)个状态,对于每个状态枚举扔鸡蛋的楼层 x,需要 O(n)的时间。这无疑在当前数据范围下是会超出时间限制的,因此我们需要想办法优化枚举的时间复杂度。此处官方采用了二分查找的思想成功优化了枚举的时间复杂度,感兴趣的小伙伴可以戳此链接:https://leetcode-cn.com/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode-solution-2/

思路拓展

  1. 关于官方的数学证明与解释,我反反复复看了好几遍都觉得难以记下,个人觉得过于复杂繁琐,于是乎不在此附上其代码和解释,而是转换思想进行简单明了的题目解答
  2. 首先抛开刚才的过程,我们可以改变一下求解的思路,求k个鸡蛋在操作次数为m时最多可以确定的楼层数量:假设dp[k][m]表示k个鸡蛋在m步内最多能测出的层数。那么,问题可以转化为当k <= K时,找一个最小的m,使得dp[k][m]<= N
  3. 我们来考虑下求解dp[k][m]的策略:
    • 假设我们有k个鸡蛋第m步时,在第X层扔鸡蛋。这时候,会有两种结果,鸡蛋碎了,或者没碎
    • 如果鸡蛋没碎,我们接下来会在更高的楼层扔,最多能确定 dp[k][m-1] 层的状态
    • 如果鸡蛋碎了,我们接下来会在更低的楼层扔,最多能确定 dp[k-1][m-1] 层的状态
    • 因此,这次扔鸡蛋,我们最多能测出 dp[k-1]m-1 + dp[k][m-1] (没摔碎时能确定的层数) + 1 (本层) 层的结果。
    • 另外,我们知道一个鸡蛋一次只能测一层,没有鸡蛋一层都不能测出来。
    • 如果移动步数m=0,那么一层都无法确定;如果鸡蛋个数为1,那么我们在m步内最多能确定m层的状态
    • 动态规划的终止条件为:此时能够确定的层数为N,返回此时的m值,为最小的移动步数
  4. 因此我们可以列出完整的递推式:
     dp[k][0] = 0
     dp[1][m] = m (m > 0)
     dp[k][m] = dp[k-1][m-1] + dp[k][m-1] + 1 (k > 0, m>0)
    

代码实现

class Solution {
public:
    int superEggDrop(int K, int N) {
        if(K==0) return 0;
        if(K==1) return N;
        int dp[N+2][K+2];
        memset(dp,0,sizeof(dp));
        dp[0][0]=0;
        for(int i=1;i<=N;++i)
        {
            dp[i][0]=0;
            for(int j=1;j<=K;++j)
            {
                dp[i][j]=dp[i-1][j]+dp[i-1][j-1]+1;
                if(dp[i][j]>=N)
                    return i;
            }
        }
        return N; 
    }
};