887. 鸡蛋掉落

https://leetcode-cn.com/problems/super-egg-drop/

解法一:dp

我们在第i层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。注意,这时候状态转移就来了: 如果鸡蛋碎了,那么鸡蛋的个数K应该减一,搜索的楼层区间应该从[1..N]变为[1..i-1]共i-1层楼; 如果鸡蛋没碎,那么鸡蛋的个数K不变,搜索的楼层区间应该从 [1..N]变为[i+1..N]共N-i层楼。

class Solution:
    def superEggDrop(self, K: int, N: 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	#在第i楼扔了一次
                    )
            # 记入备忘录
            memo[(K, N)] = res
            return res
        return dp(K, N)    #调用dp函数

改进

上面代码对用例4,5000会报超时,递归栈爆了。可以利用dp函数的单调性(见下图),将求 max(dp(K, N - i), dp(K - 1, i - 1)) 的过程优化为二分,找最低点 注意之前求的是 res = min(res, max(dp(K, N - i), dp(K - 1, i - 1)) + 1) 因此二分过程中求的dp要加一再与res比较

重写状态转移

之前定义的dp数组含义:

用 dp 数组表示的话也是一样的:

即:确定当前的鸡蛋个数和楼层数,就知道最小扔鸡蛋次数

现将dp定义修改为:确定当前鸡蛋个数和最多允许的扔鸡蛋次数,求通过这两个条件确定的最高楼层数

比如说 dp[1][7] = 7 表示: 现在有 1 个鸡蛋,允许你扔 7 次; 这个状态下最多给你 7 层楼, 使得你可以确定楼层 F 使得鸡蛋恰好摔不碎 (一层一层线性探查)

最终要求的其实是扔鸡蛋次数m,但是这时候m在状态之中而不是dp数组的结果。因此题目就是给K鸡蛋,N层楼,让你求最坏情况下最少的测试次数mwhile循环结束的条件是dp[K][m] == N

原始解法还得线性或者二分扫描所有楼层,要求最大值、最小值。但是现在这种dp定义根本不需要这些了,基于下面两个事实:

1、无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上。

2、无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)。

得状态转移方程:

由方程可知dp[k][m]依赖左边和左上的元素,因此遍历应以列为优先顺序 最后求的是使dp[K][m]>=N的最小m,因此循环退出时,要考虑dp[K][m]与N的关系

最后更新于