LeetCode-鸡蛋掉落-题号887-Java实现

1、题目链接

鸡蛋掉落

2、题目大意

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

3、样例输入

K = 1, N = 2
K = 2, N = 6
K = 3, N = 14

4、样例输出

2
3
4

5、思路

题目设定鸡蛋在高于F层楼落下就会摔碎,在低于等于F层楼落下不会摔碎,那么现在F未知,我将鸡蛋在某层楼扔下,通过鸡蛋是否摔碎来确定F的值,这样需要至少扔几次才能确定F值。
比如第一个样例输入,由于鸡蛋个数只有一个,摔碎了就没有了,所以在 F 没有确定前,我们要保证鸡蛋不会摔碎,所以只能选择最愚蠢的办法,从一楼开始扔,如果从一楼扔鸡蛋,鸡蛋碎了,那么表示 F 值为 0,如果没碎,那么从二楼扔鸡蛋,如果鸡蛋碎了,那么表示 F 值为 1,如果没碎,那么 F 值为 2。
当鸡蛋为一个的时候,我们的试错成本太高,因此只能从一楼开始往上试,那么如果鸡蛋是多个的时候,我们就可以选择从楼层中间某一层开始扔鸡蛋,比如在 1-N 层楼中选择 X 层扔鸡蛋,如果鸡蛋碎了,那么 F 肯定在 1-(X-1)层范围内,如果没碎,则在(X+1)-N层范围内,这题目就很清楚了,明显是个动态规划题。


既然是个动态规划题,那么我们先设 dp[i][j] 为拥有 i 个鸡蛋,建筑有 j 层的情况下确定 F 值至少需要的次数,那么可以确立以下动态转移方程:
dp[i][j] = 1 + m i n X min_{X} minX{ max{ dp[i][N-X], dp[i-1][X-1] } }
dp[i][j] 的值由两部分组成,首先能够确定的是,我们在中间的 X 层将鸡蛋往下扔,那么只有两种情况,鸡蛋碎了, F 在 X 层以下,也就是还需要 dp[i-1][X-1] 次才能确认 F,若鸡蛋没碎,则 F 在 X 层以上,至少还需要 dp[i][N-X] 次。
由于我们选择在 X 层扔鸡蛋的时候不能预料鸡蛋是否会碎,这是我们无法选择的,因此我们只能取这两种情况中的最大的情况,而 X 的取值对于我们来说,是可以选择的,譬如 X=a 的时候,至少需要 1 次,X=b 的时候,至少需要 2 次,那么我们肯定选择 a 层进行扔鸡蛋操作,因为我们求的是确认F的 最小移动次数
这也是为什么动态转移方程中要先取最大值,再取最小值的原因。


根据上述转移方程,可以想到用一个三重循环来进行求解,dp数组的 i 和 j 分别对 K 和 N 进行遍历,而 X 则对 1-j 进行遍历,于是时间复杂度为 O(K * N * N),再看题目要求 K 最大为100,N 最大为10000,这个时间复杂度肯定是爆了,因此需要优化。
根据官方题解的提点,可以发现 dp[i][N-X] 是关于X的递减函数,因为 X 越大,N-X 就越小,楼层高度越小,那么自然需要的次数也越少,同理 dp[i-1][X-1] 是关于X的递增函数,于是可以得到下图:
坐标图
其中蓝线为 dp[i][N-X],棕线为 dp[i-1][X-1],由于当 X 固定时,取得是两者间的最大值,于是可以得到下图:
坐标图
而这个函数的最小值便是我们所要求的X对应的次数,而这种先减后增的序列,求其中的最小值,我们可以使用二分查找来完成,我们只需要找到减序列的最后一个值或者增序列的第一个值,在我的代码里,我选择找减序列的最后一个值。这里需要注意的是,我们的序列不是真正意义上的连续增减序列,而是离散的,也就是说 X 对应的并不一定是上图中的最小值,有可能是最小值的左边一位,也有可能是最小值的右边一位,因此找到减序列的最后一个值后,还要找该值的右边一位进行比较。
于是时间复杂度就优化为了 O(K * N * log(N)),可满足题目需要。

6、代码

class Solution {
    public int superEggDrop(int K, int N) {
        int[][] dp = new int[K+1][N+1];
        for(int i=1;i<=N;i++){
            dp[1][i]=i;
        }
        for(int i=1;i<=K;i++){
            dp[i][1]=1;
        }
        for(int i=2;i<=K;i++){
            for(int j=2;j<=N;j++){
                int low = 1, high = j;
                int k = low;
                while(low<=high){
                    int mid = (low+high)>>1;
                    if(dp[i][j-mid]>=dp[i-1][mid-1]){
                        if(mid>k){
                            k=mid;
                        }
                        low = mid + 1;
                    } else {
                        high = mid - 1;
                    }
                }
                dp[i][j] = 1 + Math.min(dp[i][j-k],dp[i-1][k]);
            }
        }
        return dp[K][N];
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值