zoukankan      html  css  js  c++  java
  • 动态规划——楼层扔鸡蛋问题

    前言

    大一的时候蓝桥杯省赛遇到过(作为非编程题的压轴题),这次看的别人的面经也多次出现,就写篇博文总结一下。

    题目

    有一栋楼共100层,一个鸡蛋从第N层及以上的楼层落下来会摔破, 在第N层以下的楼层落下不会摔破。给你2个鸡蛋,设计方案找出N,并且保证在最坏情况下,最小化鸡蛋下落的次数。

    解析

    无脑二分法(最多人想到的伪解法)

    当时省赛没注意审题,就想的这种方法,首先需要确定的是,在最坏的情况下,求最小化尝试次数,所以肯定不是无脑二分那么简单了。
    例如,你第一次扔第50层,碎了如果你再选择二分,直接到25层又碎的话,两个鸡蛋就都没了,接下来你咋试啊?
    所以你接下来只能从1层到49层一个一个试了,最终尝试次数为50次。

    假设法

    首先,假设答案,也就是最小尝试次数为x,此时从第x层开始扔,有两种情况:

    • 碎了,那么只能从1到x-1一个一个试了,加上前面扔的一次,总结果为x次,符合,这也是为什么选择第x层的原因,如果选择其他层,又碎了的话,则最小尝试次数肯定不等于x,这就与假设相悖了。
    • 没碎,那么直接把第1到x层抛弃掉,当作不存在(因为鸡蛋不会在这范围内碎掉),我们把第x+1层当成第1层,尝试次数为x-1(因为刚刚扔了1次,最小尝试次数减1),此时就从第x-1层(真实层数为x+x-1)开始扔,同样又会出现两种情况:
    • 第二次碎了,则是从第1层到第x-2层开始扔 ,总尝试次数同样是x
    • 第二次没碎,还是之前原理,这次从x-2层开始,以此类推,一直扔到最后一层或碎了为止。

    最终结果就是(x+(x-1)+(x-2)...+1 = 100),解得(x = 14)

    题目升级版本

    楼层M,鸡蛋数N,求最坏情况下的最小次数。

    动态规划法

    理解了上面的假设法,再学过动态规划的话,这里应该就问题不大了。
    状态转移方程如下:

    [f[m][n] = min(f[m][n],1+max(f[k-1][n-1],f[m-k][n]),kin[1,m-1]) ]

    解释:当有n个鸡蛋时,所需尝试的楼层数为m,此时将鸡蛋扔在第k层,则有两种情况

    • 碎了,那么接下来只需要尝试1到k-1层,鸡蛋数为n-1,此时问题不就转化成了楼层数k-1,鸡蛋数n-1,求最坏情况下的最小次数吗?
    • 没碎,那么直接把第1到k层抛弃掉,只需要尝试第k+1到m层,鸡蛋没碎,所以扔为n,此时问题不就转化成了楼层数m-k,鸡蛋数n,求最坏情况下的最小次数吗?
    • 为什么取MAX?因为是最坏的情况,所以取碎了与没碎中的最大情况。

    代码如下:

    int superEggDrop(int egg,int floor){
    	int ans[floor+1][egg+1];
    	for(int m = 1;m <= floor;m++)
    		for(int n = 1;n <= egg;n++)
    			ans[m][n] = m;//最坏的情况下,自然是所有楼层试一遍,同时这也是鸡蛋数为1时的答案
    
    	for(int m = 1;m <= floor;m++)
    		for(int n = 2;n <= egg;n++)//n必须从2开始,如果是1,就会出现ans[k-1][1-1=0],显然不存在0鸡蛋的情况
    			for(int k = 1;k <= m-1;k++)
    				ans[m][n] = min(ans[m][n],1+max(ans[k-1][n-1],ans[m-k][n]));
    	return ans[floor][egg];
    }
    

    然而,该解法的时间复杂度为(O(km^2)),空间复杂度为(O(mn)),显然还可以继续优化。

    动态规划+二分优化

    对于(f[k-1][n-1])(f[m-k][n]),当在第三重循环中,(m,n)不变,我们可以将其当作系数,只有(k)([1,m-1])的范围内一直增加,而(k)又与楼层数有关,显然,当楼层数增加时,测试次数一定增加(100层和101层,显然100更有利吧?)。
    (f[k-1][n-1])(f[m-k][n]),前者(k)系数为正,后者(k)系数为负,一个递增,一个递减,我们就可以找二分它们的交点,使得无论碎不碎,它们的测试结果都相同,使得时间复杂度为(O(kmlogm))

    int superEggDrop(int egg,int floor){
    	int ans[floor+1][egg+1];//鸡蛋数只需要考虑两种情况
    	for(int m = 1;m <= floor;m++)
    		for(int n = 1;n <= egg;n++)
    			ans[m][n] = m;//最坏的情况下,自然是所有楼层试一遍,同时这也是鸡蛋数为1时的答案
    
    	for(int m = 2;m <= floor;m++)//当楼层数为1时,结果必然是1
    		for(int n = 2;n <= egg;n++){//n必须从2开始,如果是1,就会出现ans[k-1][1-1=0],显然不存在0鸡蛋的情况
    			int l = 1,r = m;//范围是[l,r)
    			while(l+1 < r){
    				int k = (l+r)/2;
    				int l_value = ans[k-1][n-1];
    				int r_value = ans[m-k][n];
    				if (l_value == r_value){
    					l = k;
    					break;
    				}
    				else if (l_value > r_value) r = k;
    				else l = k+1;
    			}
    			ans[m][n] = min(ans[m][n],1+max(ans[l-1][n-1],ans[m-l][n]));
    		}
    	return ans[floor][egg];
    }
    

    当然了,现在还不是最优解,由于时间问题,这里就不再赘述,有兴趣的可以自行百度。

  • 相关阅读:
    Android笔记(adb命令--reboot loader)
    Android笔记(预安装APK)
    Linux驱动学习(编写一个最简单的模块)
    const关键字与指针
    C++函数重载遇到了函数默认参数情况
    uboot环境变量分析
    ftp服务
    Samba服务
    mariadb_2 单表的增删改查
    mariadb_1 数据库介绍及基本操作
  • 原文地址:https://www.cnblogs.com/MMMMMMMW/p/12406172.html
Copyright © 2011-2022 走看看