其他分享
首页 > 其他分享> > 高楼扔鸡蛋问题(相关话题:动态规划)

高楼扔鸡蛋问题(相关话题:动态规划)

作者:互联网

前言

但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承咱们号一贯的作风,拒绝奇技淫巧,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了不太划算

问题描述

一幢 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单增的,如果我们固定KN把这两个函数看做关于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