动态规划+二分查找
首先我们根据dp(K, N)
数组的定义(有K
个鸡蛋面对N
层楼,最少需要扔 dp(K, N) 次),很容易知道K
固定时,这个函数随着N
的增加一定是单调递增的,无论你策略多聪明,楼层增加的话,测试次数一定要增加。
注意dp(K - 1, i - 1)
和dp(K, N - i)
这两个函数,其中i
是从 1 到N
单增的,如果我们固定K
和N
,把这两个函数看做关于i
的函数,前者随着i
的增加应该也是单调递增的,而后者随着i
的增加应该是单调递减的:
求二者的较大值,再求这些最大值之中的最小值,其实就是求这两条直线交点,也就是红色折线的最低点嘛。这个时候就可以用二分搜索来找这个最低点
回顾这两个dp
函数的曲线,我们要找的最低点其实就是这种情况:
for (int i = 1; i <= N; i++) {
if (dp(K - 1, i - 1) == dp(K, N - i))
return dp(K, N - i);
}
即:
lo, hi = 1, N
while lo <= hi:
mid = (lo + hi) // 2
broken = dp(K - 1, mid - 1) # 碎
not_broken = dp(K, N - mid) # 没碎
# res = min(max(碎,没碎) + 1)
if broken > not_broken:
hi = mid - 1
res = min(res, broken + 1)
else:
lo = mid + 1
res = min(res, not_broken + 1)
memo[(K, N)] = res
return res
如果用dp数组来表示上文dp函数的定义即是:
dp[k][n] = m
# 当前状态为 k 个鸡蛋,面对 n 层楼
# 这个状态下最少的扔鸡蛋次数为 m
确定当前的鸡蛋个数和面对的楼层数,就知道最小扔鸡蛋次数。最终我们想要的答案就是dp(K, N)
的结果。
- 重新定义dp数组的定义,确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定
F
的最高楼层数。
dp[k][m] = n
# 当前有 k 个鸡蛋,可以尝试扔 m 次鸡蛋
# 这个状态下,最坏情况下最多能确切测试一栋 n 层的楼
# 比如说 dp[1][7] = 7 表示:
# 现在有 1 个鸡蛋,允许你扔 7 次;
# 这个状态下最多给你 7 层楼,
# 使得你可以确定楼层 F 使得鸡蛋恰好摔不碎
# (一层一层线性探查嘛)
这里m是一个次数上界,下面我们就是通过将dp[k][m]
逼近楼层数,来找到这个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
减一,也就是鸡蛋碎了,同时扔鸡蛋次数m
减一。
PS:这个m
为什么要减一而不是加一?之前定义得很清楚,这个m
是一个允许的次数上界,而不是扔了几次。
即:
int superEggDrop(int K, int N) {
// m 最多不会超过 N 次(线性扫描)
int[][] dp = new int[K + 1][N + 1];
// base case:
// dp[0][..] = 0
// dp[..][0] = 0
// Java 默认初始化数组都为 0
int m = 0;
while (dp[K][m] < N) {
m++;
for (int k = 1; k <= K; k++)
dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1;
}
return m;
}
代码
class Solution {
public int superEggDrop(int k, int n) {
int[][] dp=new int[k+1][n+1];
int m=0;
while(dp[k][m]<n){
m++;
for(int i=1;i<=k;i++){
//楼层数=上层+下层+本层
dp[i][m]=dp[i][m-1]+dp[i-1][m-1]+1;
}
}
return m;
}
}