很经典的题目,力扣上的链接:887. 鸡蛋掉落
题目大意是:有K个鸡蛋,N层楼,求你找出在最坏情况下最小的尝试次数找出鸡蛋碎的临界楼层。
解析
理解题目建议看看李永乐老师的视频:复工复产找工作?先来看看这道面试题:双蛋问题
如果不限制鸡蛋次数,想找出最少的尝试次数,直接二分区间即可,比如一共8层,第一次看看4层碎不碎,4层不碎,去6层扔…,4层碎了,去2层扔看看碎不碎…
何为最坏情况:鸡蛋破碎一定发生在搜索区间穷尽时,同时也要保证在该种情况下使得尝试次数最少。
现在给了鸡蛋的限制,只有K个,这个时候就不能直接用二分了。比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?你直接去第 4 层扔一下,如果鸡蛋没碎还好,但如果碎了你就没有鸡蛋继续测试了,无法确定鸡蛋恰好摔不碎的楼层 F 了。这种情况下只能用线性扫描的方法,算法返回结果应该是 7 [ 1 ] ^{[1]} [1]
其实这样的话,如果使用三分,五分,十分都可以尽可能的减少尝试次数,无法直接找出最优策略,我们可以使用计算进行求解,也就是暴力枚举,在任意一个区间 [ i , j ] [i,j] [i,j],分别尝试从k层扔下鸡蛋,其中 k ∈ [ i , j ] k\in[i,j] k∈[i,j],慢慢递推,也就是动态规划的思想。
d p [ K ] [ N ] dp[K][N] dp[K][N]表示 K K K个鸡蛋, N N N层楼,找出最坏情况下最少的尝试次数的大小,假设在第i层扔鸡蛋:
- 如果碎了,此时剩下 K − 1 K-1 K−1个鸡蛋,临界就要从下面的 i − 1 i-1 i−1层楼里面找,问题就变成了 d p [ K − 1 ] [ i − 1 ] dp[K-1][i-1] dp[K−1][i−1]
- 如果没碎,还是有 K K K个鸡蛋,临界就要从上面的 N − i N-i N−i层楼里面找,问题就变成了 d p [ K ] [ N − i ] dp[K][N-i] dp[K][N−i]
无论碎没碎,扔这个鸡蛋我们都尝试了一次,所以最后要加1。
d
p
[
K
]
[
N
]
=
min
i
∈
[
1
,
N
]
{
max
(
d
p
[
K
−
1
]
[
i
−
1
]
,
d
p
[
K
]
[
N
−
i
]
)
+
1
}
dp[K][N] = \min_{i\in[1,N]}\{ \max(dp[K-1][i-1],dp[K][N-i])+1\}
dp[K][N]=i∈[1,N]min{max(dp[K−1][i−1],dp[K][N−i])+1}
无优化
class Solution {
public:
int superEggDrop(int K, int N) {
int dp[K+1][N+1];
memset(dp,0,sizeof(dp));
// 初始化
for(int i=1;i<=K;i++) dp[i][1] = 1;
for(int i=1;i<=N;i++) dp[1][i] = i;
for(int i=2;i<=K;i++){
for(int j=2;j<=N;j++){
dp[i][j] = 100000000;
for(int k=1;k<=j;k++){
dp[i][j] = min(dp[i][j],max(dp[i-1][k-1],dp[i][j-k])+1);
}
}
}
return dp[K][N];
}
};
时间复杂度: O ( K N 2 ) O(KN^2) O(KN2) 空间复杂度: O ( K N ) O(KN) O(KN)
二分优化
class Solution {
public:
int superEggDrop(int K, int N) {
int dp[K+1][N+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=K;i++) dp[i][1] = 1; // i个鸡蛋,1层楼高,最坏一次
for(int i=1;i<=N;i++) dp[1][i] = i; // 1个鸡蛋,i层楼高,最坏i次
for(int i=2;i<=K;i++){
for(int j=2;j<=N;j++){
int left = 1, right = j,up,down;
dp[i][j] = 100000000;
while(left<=right){
int mid = (left+right)/2;
up = dp[i][j-mid]; // 第mid层,扔鸡蛋没碎
down = dp[i-1][mid-1]; // 第mid层,扔鸡蛋碎了,剩下i-1个鸡蛋
if(up>down) {
dp[i][j] = min(dp[i][j],up+1); left = mid +1;
}
else if(up<down) {
dp[i][j] = min(dp[i][j],down+1); right = mid-1;
}
else {
dp[i][j] = min(dp[i][j],up+1); break;
}
}
}
}
return dp[K][N];
}
};
时间复杂度: O ( K N l o g N ) O(KNlogN) O(KNlogN) 空间复杂度: O ( K N ) O(KN) O(KN)
逆向思维再优化 [ 2 ] ^{[2]} [2]
本题应该逆向思维,若你有 K 个鸡蛋,你最多操作 F 次,求 N 最大值。
dp数组换一种表示含义,dp[i][j]表示i个鸡蛋,尝试j次,在最坏情况下能确定的楼层数。
- 假如碎了还有i-1个鸡蛋,还有j-1次尝试机会,就是dp[i-1][j-1]
- 假如没碎还有i个鸡蛋,还有j-1次机会,就是dp[i][j-1]
无论碎还是没碎,尝试的这一次已经试探出了一层楼所以要加一
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
+
d
p
[
i
]
[
j
−
1
]
+
1
dp[i][j] = dp[i-1][j-1]+dp[i][j-1]+1
dp[i][j]=dp[i−1][j−1]+dp[i][j−1]+1
class Solution {
public:
int superEggDrop(int K, int N) {
int dp[K+1][N+1]; // dp[i][j]表示i个鸡蛋,尝试j次,最坏情况下可以试探出的最高楼层
memset(dp,0,sizeof(dp));
int ans = 0; // 尝试次数
while(dp[K][ans]<N){
ans ++;
for(int k=1;k<=K;k++) dp[k][ans] = dp[k-1][ans-1]+dp[k][ans-1]+1;
}
return ans;
}
};
时间复杂度: O ( K N ) O(K\sqrt N)\quad O(KN)空间复杂度: O ( K N ) O(KN) O(KN)
逆向思维 空间优化版本 [ 3 ] ^{[3]} [3]
又因为第 j j j次尝试只和第 j − 1 j-1 j−1次尝试结果相关,因此可以只用一维数组。
class Solution {
public:
int superEggDrop(int K, int N) {
int dp[K+1];
memset(dp,0,sizeof(dp));
int ans = 0; // 尝试次数
while(dp[K]<N){
ans ++;
for(int k=K;k>0;k--) // 从后往前计算
dp[k] = dp[k-1]+dp[k]+1;
}
return ans;
}
};
时间复杂度: O ( K N ) O(K\sqrt N)\quad O(KN)空间复杂度: O ( K ) O(K) O(K)
Ref
[1] 经典动态规划问题:高楼扔鸡蛋
[2] 2004 - 朱晨光:《优化,再优化!——从《鹰蛋》一题浅析对动态规划算法的优化》
[3] 看到了提交第一的(java),真是把dp用的淋漓尽致。。。