文章目录

  • 问题描述
  • 思路1
    • 首先想到的思路(二分法、十分法、其他方法···)
    • 动态规划三要素
  • 解答1:
  • 思路1的改进:使用二分法

问题描述

这是LeetCode的第887题。题目如下:

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

示例 1:
输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。

示例 2:
输入:K = 2, N = 6
输出:3

示例 3:
输入:K = 3, N = 14
输出:4

提示:
1 <= K <= 100
1 <= N <= 10000

思路1

这道题说是DP中的高难度题不为过吧…首先要注意一点,鸡蛋不碎是可以再用的。

首先想到的思路(二分法、十分法、其他方法···)

乍一看,要找到扔鸡蛋不碎的临界楼层(此楼层及上面的楼层扔鸡蛋都会碎,下面的都不碎),大多数人(我就是这样想的)想到的应该是二分查找吧。但仔细想想,二分查找只能适用于鸡蛋无限的情况下:假设为100层楼,鸡蛋无限,可以从第50层扔一下试试,碎了就去25层再扔一个,没碎就去75层再扔一个试试。这样只需要尝试logN次一定能找到临界楼层。

但鸡蛋数量有限的情况下就不能这样扔了:还是100层楼,有2个鸡蛋,当从50层扔鸡蛋碎了,手里只剩1个鸡蛋了,这时候只能确定临界楼层在[1,50)之间,只能从第1层开始往第49层一层一层试了。鸡蛋可能在第1层就碎了,但这不是最坏情况,最坏情况就是要保证不管临界楼层在哪一层,所求出的最少尝试次数都能找到这个楼层。这里的最坏情况是临界楼层是49层,需要再尝试49次,加上一开始在50层扔碎的一个鸡蛋,一共是50次。所以,二分法是不合适的。

既然二分法每次折半查找不合适,那如果减小查找的间距呢?每次不再是二分,而是十分法,即每次跨越10层楼层查找:先在10层扔一下,没碎就去20层再扔,没碎再去30层扔…当只剩一个鸡蛋时就开始一层一层试,这样的最坏情况就是第100层都不会碎,一共尝试了以下楼层:10,20,30,40,50,60,70,80,90,91,92,93,94,95,96,97,98,99,100,共19次。但这是最优解吗?不是的,还有其他的方案。

由上面的十分法想一下,当扔到第90层鸡蛋还没碎的情况下,我现在还有两个鸡蛋,第91-100层一定要一层一层试吗?当然是不需要的,因为我只需要在只剩一个鸡蛋的情况下才需要一层层尝试。所以,最后的91-100层我可以再二分查找啊:先在95层扔一下,没碎,那再去98层扔一下,也没碎,那再去99层扔一下,也没碎,再去100层,还是没碎,这样一共尝试了前面的10、20、30、40、50、60、70、80、90加上95、98、99、100,一共尝试了13次,发现这个解比起上次的19次更优了。但注意!!如果某一层鸡蛋碎了呢?那么只剩一个鸡蛋了,就只能一层一层试了。所以,最坏情况下的临界楼层是根据查找方式而变化的,也就是不管临界楼层在哪,不管用什么查找方法,这个求得的最小次数最终一定能找到它。

那么问题又来了,既然最后91-100层可以改变查找方式,那我在前面的部分也可以改变查找方式啊。比如使用动态查找方式,我每次查找都改变间距,不再是每次跳跃十层,而是第一次跳k层,第二次跳k-1层,第三次跳k-2层…最后一次跳1层。这个k不是固定的,使用不同的k获得的查找次数也不一样。只要满足k + (k-1) + (k - 2) + (k - 3) + ··· + 1 >=楼层数N即可。如下图:

这样的话,第一次我需要在15楼扔个鸡蛋试试,没碎去28楼再扔一个······一共尝试15、28、40、51、61、70、78、85、91、96、100,共11次,这种方案也比最开始的19次要更优。

再比如,在前50层中进行十分查找,后五十层进行二分查找;或者前20层中进行二分查找,20-40层进行四分查找,40-60层进行八分查找…或者每次跳跃的k不同······方案有无数种,我们要求的就是不管使用什么查找方案,最终找到这个最少次数,对于在任何位置的临界楼层都能找到它。

动态规划三要素

1.dp数组的含义:现在需要考虑的状态,一个是手里还有几个鸡蛋,一个是当前楼有多少层。使用dp[n][k]表示n层楼,手里还有k个鸡蛋时,至少需要试多少次才能找到临界楼层。

2.初始值:当楼层数为0的时候,就是没有任何楼层的时候,不管手里有几个鸡蛋,都不需要尝试,即dp[0][*] = 1;当手里鸡蛋只剩一个时,不管剩下多少层,只能一层一层试,那么有多少层楼就需要试多少次,即dp[k][1] = k。列出如下表格,左边一列是楼有多少层,上边一行是手里有多少个鸡蛋:(这里其实可以不考虑n = 0的情况,直接从n = 1开始,初始值dp[1][k] = 1,因为楼只有一层,不管有多少个鸡蛋,都只需要试一次)

3.状态转移方程:假设我站在第n层扔鸡蛋,手里有k个鸡蛋,那么这个鸡蛋扔下去,就会有两种结果:碎或者不碎。如果碎了,我就需要去第n层下面的1~n-1层中再试,楼层数剩下n-1层,此时手里的鸡蛋数为k-1;如果没碎,我就需要去第n层上面的n+1~N层中再试,楼层数剩下N-(n+1)+1共N-n层,此时手中的鸡蛋数为k。这里可以把上下两部分想像成两栋楼,一栋楼共是n-1层,另一栋楼共N-n层。本来需要做的选择是在第n层楼的上下两段中选择次数较多的一段,现在看是在两栋楼中选择次数较多的一次,这个选择等同于人为地去设置鸡蛋碎还是不碎,因为题目要求的是最差的情况下,所以,去上面楼层(第二栋楼)继续试还是去下面楼层(第一栋楼)继续试,哪栋楼需要试的次数多就选哪种,i.e.在上面楼层(第二栋楼)需要尝试的次数多,那就让鸡蛋不碎;在下面楼层(第一栋楼)尝试的次数多,那就让鸡蛋碎。此时的状态转移方程表示楼高n层,手里有k个鸡蛋时找到临界楼层的最少次数:dp[n][k] = max{dp[n - 1][k - 1], dp[N - n][k]} + 1(+1是加上在第n层扔的这一次),dp[n - 1][k - 1]dp[N - n][k]的值都是已知的了。

这里需要注意一点:不要关注第n层楼上面下面的第几层楼,而是第n层楼上面下面还剩下多少层楼需要尝试!假设一共10层,第一次在6层扔了一次,那么需要考虑的就是下面有5层需要尝试、上面还有4层需要尝试,在当前手里有k个鸡蛋的情况下,去哪边尝试扔的次数更多一些。这里下面的5层和上面的4层可以想象成是两栋楼,一栋有5层,一栋有4层,手里还有k个鸡蛋,去哪栋楼扔会扔的次数更多。那么从哪层楼开始扔呢?从第1层楼开始,一直扔到第N层挨层试,每次都取当前楼层的上下两部分中需要尝试次数较多的那部分的次数,这样才能保证在当前楼层扔完后,所选的这个次数能适用于最坏的情况,即适用于所有情况。现在得到:第一次在第1层扔、第一次在第2层扔、第一次在第3层扔······第一次在第N层扔的N个次数值,它们都能保证第一次在当前楼层扔时,最坏情况下都能找到临界楼层,但是第一次在哪里扔我是可以选的,当然是第一次在哪层楼扔需要尝试的次数更少我就在哪扔第一次,即从这N个值中选最小的那个。

解答1:

下面着手写代码:首先,遍历楼层数N,从一层高的楼开始,一直到N层高的楼;其次,从手里有2个鸡蛋开始,遍历到手里有K个鸡蛋;每次需要做什么呢?即求出dp[n][k]。要求出dp[n][k],就需要挨着尝试第一次把鸡蛋扔在第几层更好,从第1层开始,一直试到当前的楼层高n。

C++代码如下:

int superEggDrop(int K, int N) {vector<vector<int>> dp(N + 1, vector<int>(K + 1, 0));// 初始化dp数组for (int n = 0; n <= N; n++) dp[n][1] = n;//状态转移方程for (int n = 1; n <= N; n++)for (int k = 2; k <= K; k++) {int res = INT_MAX;for (int j = 1; j <= n; j++)//第一次从哪扔?从第1层开始试res = min(res, max(dp[j-1][k-1], dp[n-j][k]) + 1);dp[n][k] = res;}return dp[N][K];}

但是,这种方法的时间复杂度很高。由于枚举N层高的楼、手里K个鸡蛋的情况共有KN种状态,每种状态都需要从第1层楼开始挨层尝试一遍,所以时间复杂度为O(KN*N),即O(KN^2)。这个时间复杂度是无法通过LeetCode这道题的提交的。所以,需要着手优化。

思路1的改进:使用二分法

这里的二分法并不是前面所提到的那种二分法。待改进的部分是最内层的循环,即第一次从哪层扔时,不再是从第1层开始挨着试,而是使用二分法试。

为什么可以这样做呢?观察dp[n][k] =min{ max{dp[n - 1][k - 1], dp[N - n][k]} + 1}这个方程,在鸡蛋数k不变、总楼层数N不变的情况下,自变量n在[1, N]变化时,第一个函数dp[n - 1][k - 1]是单调递增的,因为手里鸡蛋数量固定为k-1时,当前楼层数越多,我需要试的次数就越多,函数最小值为:n = 1时,最小值为dp[0][k - 1] = 0;最大值为:n = N时,此时最大值为dp[N - 1][k - 1]。第二个函数dp[N - n][k]在手里鸡蛋数量固定为k时,自变量n从[1, N]变化时是单调递减的,因为当前楼层数n越大,剩下的楼层数N - n就越小,需要试的次数就越少,最小值为:n = N时,dp[0][k] = 0;最大值为:n = 1时,dp[N - 1][k]。这两个函数是离散函数,为了方便在图中表示,这里把它想像成连续函数。如下图:
那么,两个函数的较大值max{dp[n - 1][k - 1], dp[N - n][k]}便是图中蓝色标注的部分,我把它定义为Z(x),即Z(x) = max{dp[n - 1][k - 1], dp[N - n][k]},那么,Z(x)的自变量x的取值便是[1, n],同样把这个离散函数Z(x)当作连续函数看待,那么min{Z(x)}的最小值就是前面两个函数相交的点。现在,就需要用二分法来找这个最小值点。

首先要明确的是,不管n取[1, N]中的任何值,两个函数的交点一定存在,现在把之前的从n = 1开始试的代码改为在楼高为n时,在[1, n]范围内找交点。由于真实函数是离散的,所以两个函数的交点不一定重合,可能出现连续函数交点的附近,要么左边要么右边。由于我们已知一开始dp[n - 1][k - 1]是小于dp[N - n][k]的,所以只要找到dp[n - 1][k - 1]大于等于dp[N - n][k]的那个点即可定位交点的范围。初始时,low为1,high为n,若在n = mid点的函数值还是dp[mid - 1][k - 1]<dp[n - mid][k],则说明交点出现在mid的右边,这时就让low指向mid + 1的位置,并记录此时更小的Z(x)函数值,然后继续查找;直到找到有一个点dp[n - 1][k - 1]dp[N - n][k],此时说明交点在mid的左边,则让high指向mid - 1的位置,记录此时更小的Z(x)函数值。特殊的情况是两个离散的函数刚好有交点,此时这个交点对应的函数值一定是Z(x)的最小值,就不用再查找了。最后将最小值加上在交点这一层扔的一次鸡蛋即可。

C++代码如下:

class Solution {public:int superEggDrop(int K, int N) {if (K == 1) return N;if (N == 1) return 1;vector<vector<int>> dp(N + 1, vector<int>(K + 1, 0));// 初始化dp数组for (int n = 1; n <= N; n++) dp[n][1] = n;// for (int k = 1; k <= K; k++) dp[1][k] = 1;for (int n = 1; n <= N; n++) {for (int k = 2; k <= K; k++) {int low = 1, high = n, mid;int res = dp[n-1][k];//初始值设置为dp[N-n][k]函数的最大值//下面使用二分查找while (low <= high) {mid = low + (high - low) / 2;// mid = low + ((high - low) >> 1);if (dp[mid-1][k-1] == dp [n-mid][k]){//刚好找到交点res = min(res, dp[mid-1][k-1]);break;} else if (dp[mid-1][k-1] > dp[n-mid][k]){//确定交点在[low, mid]之间high = mid - 1;res = min(res, dp[mid-1][k-1]);} else if (dp[mid-1][k-1] < dp[n-mid][k]){//确定交点在[mid, high之间low = mid + 1;res = min(res, dp[n-mid][k]);}}dp[n][k] = res + 1;//最后加上在交点这一层扔的一次}}return dp[N][K];
}
};

参考:
1.李永乐B站 双蛋问题
2.labuladong的算法小抄:《经典动态规划:高楼扔鸡蛋》
3.LeetCode评论区:@Zack的评论
4.BRILLIANT

动态规划之《高楼扔鸡蛋》问题详解 LeetCode 887.鸡蛋掉落相关推荐

  1. java动态规划鸡蛋问题_动态规划系列/高楼扔鸡蛋问题.md · lipengfei/fucking-algorithm - Gitee.com...

    # 经典动态规划问题:高楼扔鸡蛋 今天要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼.国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋 ...

  2. 算法学习笔记——动态规划:高楼扔鸡蛋

    LeetCode 887. 鸡蛋掉落 建筑有n层(取值1,2,...n),存在一个楼层F(0<=F<=n,注意F取值比n多一个),从高于F的楼层扔鸡蛋,鸡蛋会碎:否则鸡蛋不会碎 给你k枚相 ...

  3. 经典动态规划:高楼扔鸡蛋

    点击蓝色"五分钟学算法"关注我哟 加个"星标",天天中午 12:15,一起学算法 作者 | labuladong 来源 | labuladong 今天要聊一个很 ...

  4. 装鸡蛋的鞋子java代码_Java实现 LeetCode 887 鸡蛋掉落(动态规划,谷歌面试题,蓝桥杯真题)...

    887. 鸡蛋掉落 你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑. 每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去. 你知道存在楼层 F ,满足 0 < ...

  5. LeetCode 887. 鸡蛋掉落(DP,难、不懂)

    1. 题目 你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑. 每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去. 你知道存在楼层 F ,满足 0 <= F ...

  6. 动态规划:高楼扔鸡蛋

    方法一:常规动态规划 1.dp[i][j]数组定义:dp[i][j]表示鸡蛋数量为i,可选楼层数为j的状态下确定F的最少步数,问题所求为dp[K][N] 2.base case:当楼层数为0,步数为0 ...

  7. 动态规划经典入门级题目*2及详解

    题目来自极客学院的视频教程-动态规划(一) 一句话解释动态规划:多阶段最优化决策解决问题的过程 解题技巧:只关注状态转移(子问题之间的关系),不要考虑某个状态是怎么出现的 1.字符串解码 一个只包含大 ...

  8. 0x56. 动态规划 - 状态压缩DP(习题详解 × 7)

    目录 Problem A. 最短Hamilton路径 ProblemB. 蒙德里安的梦想 Problem C. Corn Fields Problem D. 小国王 Problem E. 炮兵阵地 P ...

  9. 动态规划之0-1背包问题(思路详解+表格演示过程+最优解打印方法+详细代码)

    问题简介 输入:n种物品和一个背包 物品i的重量是wi,价值为vi 背包的容量是C 输出:装入背包的物品 优化目标是:装入背包的物品总价值最大 优化子结构 设(x1,x2,x3-xn)是0-1背包问题 ...

  10. leetcode.887 鸡蛋掉落(Hard)

    动态规划方法一: 考虑每一层扔鸡蛋,扔下去的结果两种,碎.不碎 如果碎了,说明F在 1~ i-1 之间,还需要在1~ i-1即i-1层楼中搜索 如果没有碎,说明F在 i~ n之间,还需要再i+1~n层 ...

最新文章

  1. 速度×6,模型缩小20倍:用华为华科的TinyBERT做搜索,性能达BERT 90%
  2. JavaScript创建Map对象(转)
  3. FAILED: ValidationFailureSemanticException partition spec {dt=2021-04-01} doesn‘t contain all (2) pa
  4. Excel 工作表,单元格破解密码宏
  5. 收藏 | 10个重要问题概览Transformer全部内容
  6. Java 语言 ArrayList 和 JSONArray 相互转换
  7. sklearn 模型选择和评估
  8. python笔试编程题_编程笔试题(四)栈
  9. 亲身经历:2018年深圳保洁员工资时薪75,月薪不清楚
  10. Websocket 从header读取数据
  11. 大气压随温度变化表_空气密度随温度的变化
  12. Java日期查询:日、周、旬、月、季度、年等时间操作
  13. ThinkPad 交换 Ctrl 键和 Fn 键
  14. 基于Openfire Smack开发即时通讯应用、获取离线消息,发送消息,联系人列表,添加好友(三)
  15. Druid 统计监控页面无法打开
  16. C++:从入门到放弃[1]基础知识
  17. 江苏大学计算机学院复试题,本部基础A定稿-含答案(江苏大学计算机).doc
  18. unity工程文件在备份时可以删除掉三个文件夹
  19. 降噪耳机哪个牌子好?商务用蓝牙降噪耳机推荐
  20. 计算机与文秘专业有哪些课程,文秘专业开设的课程有哪些

热门文章

  1. 电驴服务器更新的作用,用电驴,这些服务器知识你必知
  2. 2.4 货币转换 B
  3. python画聚类树状图_聚类分析python画树状图--Plotly(dendrogram)用法解析
  4. 计算机磁盘的卷是什么意思,磁盘卷和分区的不同是什么?
  5. 屠神-官方正版折扣端全面评测报告
  6. 考研计算机320分什么水平,考研320分算什么水平,能上211、985吗?很多人都答不上...
  7. 软件测试设计之——场景设计法,判定表法
  8. excel高级筛选怎么用_Excel高级筛选使用
  9. 利用Matlab筛选给定条件的数据
  10. JavaWeb手机短信验证,使用Bmob进行手机短信验证,JavaScript实现手机短信验证