高楼扔鸡蛋问题(相关话题:动态规划)
作者:互联网
前言
但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承咱们号一贯的作风,拒绝奇技淫巧,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了不太划算
问题描述
一幢 n层的大楼,给你k个鸡蛋. 如果在第 i层扔下鸡蛋,鸡蛋不碎,那么从前 i-1 层扔鸡蛋都不碎.这k(k>1)只鸡蛋一模一样,不碎的话可以扔无数次. 已知鸡蛋在0层扔不会碎.提出一个策略, 要保证能测出鸡蛋恰好不会碎的楼层, 并使此策略在最坏情况下所扔次数最少.
问题分析
1)最坏情况下所扔次数最少,比较绕口。想表达的意思是,在不明确知道哪一层会碎的情况下,要找到一种策略,通过最少的试验次数,得到临界楼层(恰好不会碎的楼层)。不明确知道,就需要考虑最糟糕的情况,而且这种策略与其他策略相比是最糟糕的情况下,最少的试验次数。
2)假设一种扔法:第一个鸡蛋,从50楼扔下去。如果碎了,第二个鸡蛋必须从1~49层逐层试验。如果第i层为临界层,且i≤49,这个时候,要试验的总次数是1 +(i - 1)。因为必须保证在没找到临界楼层之前,鸡蛋不能碎。如果没碎,则第一个鸡蛋可以接着从75层扔。因为即使这次碎了,还有k-1个鸡蛋,可以继续试验。对第一个鸡蛋的继续从中间分,就比较合理。
3)假设到代数:如果第一枚鸡蛋扔下去的层数为i,则碎了的情况,需要扔的总次数最糟糕的情况是1 + ( i - 1 );如果没碎,剩下的k-1个鸡蛋都在,需要扔的次数一定为1 + 用k枚鸡蛋来解决剩下的n- i层的次数(这个问题跟原题是一样的,但是层数少了一些)
4)我们在第i
层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。注意,这时候状态转移就来了:
如果鸡蛋碎了,那么鸡蛋的个数K
应该减一,搜索的楼层区间应该从[1..N]
变为[1..i-1]
共i-1
层楼;
如果鸡蛋没碎,那么鸡蛋的个数K
不变,搜索的楼层区间应该从 [1..N]
变为[i+1..N]
共N-i
层楼。
把这个问题图形化理解如下:
dp(k,n)表示剩余k鸡蛋,n层楼的情况下的最少尝试次数
代码实现
写出伪代码
for (int i = 1; i <=n; i++) {
res = Math.min(res, Math.max(dp(k - 1, i - 1), dp(k, n - i)) + 1);
}
dp(k,n)=res
像火影里的须佐能乎一样,接下来完善骨架
解法一
下面是一个超时的解法
public class 高楼扔鸡蛋问题 {
private static int k = 2;
private static int n = 100;
private int mem[][] = new int[k][n];
// 函数本身的复杂度就是忽略递归部分的复杂度,这里dp函数中有一个 for 循环,所以函数本身的复杂度是 O(N)。
// 子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。
// 所以算法的总时间复杂度是 O(K*N^2), 空间复杂度为子问题个数,即 O(KN)
public Integer dp(int k, int n) {
// 当楼层数N等于 0 时,显然不需要扔鸡蛋
// 当楼层数N等于 1 时,只需要扔一次鸡蛋
//当鸡蛋数K为 1 时,显然只能线性扫描所有楼层:
if (k == 1) {
return n;
}
if (n == 1 || n == 0) {
return n;
}
if (mem[k - 1][n - 1] != 0) {
return mem[k - 1][n - 1];
}
int res =Integer.MAX_VALUE;
for (int i = 1; i <=n; i++) {
// Math.max的目的是计算最坏情况下的扔鸡蛋次数
res = Math.min(res, Math.max(dp(k - 1, i - 1), dp(k, n - i)) + 1);
}
mem[k - 1][n - 1] = res;
return res;
}
public static void main(String[] args) {
高楼扔鸡蛋问题 test = new 高楼扔鸡蛋问题();
System.out.println(test.dp(k, n));
}
}
细节思考
为什么用一个 for 循环遍历楼层[1..N]
比方说你有 2 个鸡蛋,面对 10 层楼,你得拿一个鸡蛋去某一层楼扔对吧?那选择去哪一层楼扔呢?不知道,那就把这 10 层楼全试一遍。至于鸡蛋碎没碎,下次怎么选择不用你操心,有正确的状态转移,递归会算出每个选择的代价,我们取最优的那个就是最优解。
其实,这个问题还有更好的解法,比如修改代码中的 for 循环为二分搜索,可以将时间复杂度降为 O(K*N*logN);再改进动态规划解法可以进一步降为 O(KN);使用数学方法解决,时间复杂度达到最优 O(K*logN),空间复杂度达到 O(1)。
解法二
如果能够理解这个状态转移方程,那么就很容易理解二分查找的优化思路。
首先我们根据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
的增加应该是单调递减的:
下面的代码需要结合上面的图进行理解
public Integer dp2(int k, int n) {
// 当楼层数N等于 0 时,显然不需要扔鸡蛋
// 当楼层数N等于 1 时,只需要扔一次鸡蛋
// 当鸡蛋数K为 1 时,显然只能线性扫描所有楼层:
if (k == 1) {
return n;
}
if (n == 1 || n == 0) {
return n;
}
int low = 1;
int hight = n;
int res = Integer.MAX_VALUE;
while (low <= hight) {
int mid = (low + hight) / 2;
int broken = dp(k - 1, mid - 1);
int notBroken = dp(k, n - mid);
if (broken > notBroken) {
hight = mid - 1;
//由于要考虑最坏情况所以要用较大的broken来比较
//加1表示当前mid楼层所做的实验
res = Math.min(res, broken + 1);
} else {
low = mid + 1;
res = Math.min(res, notBroken + 1);
}
}
return res;
}
参考文章
https://mp.weixin.qq.com/s/xn4LjWfaKTPQeCXR0qDqZg
https://mp.weixin.qq.com/s/7XPGKe7bMkwovH95cnhang
标签:int,res,鸡蛋,话题,楼层,高楼,复杂度,dp 来源: https://blog.51cto.com/u_13270164/3035925