[路飞]_leetcode-887-鸡蛋掉落

简介: leetcode-887-鸡蛋掉落

网络异常,图片无法展示
|


[题目地址][B站地址]


给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。


已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。


每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。


请你计算并返回要确定 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 <= 104


前置信息


为了方便小伙伴们理解鸡蛋如何扔,我们先看一下固定楼层和鸡蛋数量的时候,如何求 f ?


网络异常,图片无法展示
|


可以看到,只有一枚鸡蛋的时候,为了稳妥的找到 f,我们只能从第一层一层一层向上尝试,而有多枚鸡蛋的时候,我们就可以拆分区间,大大提升查找的效率。


初版动态规划


解题思路


首先这种给定动态条件,求最值的问题,肯定可以通过动态规划来解题。


本题中的动态条件有两个,k 枚鸡蛋,n 层楼。


所以我们可以定义一个二维 dp => dp[i][j] ,表示 i 枚鸡蛋和 j 层楼所需要的最少操作次数。


接下来我们思考一下如何推导 dp[i][j] 呢?


其实扔鸡蛋的结果无非只有两种,鸡蛋碎了以及鸡蛋没碎。


所以我们假设当前在第 x (1<=x<=j) 层扔鸡蛋,


如果鸡蛋碎了,那么 dp[i][j] = dp[i-1][x-1]+1,也就是鸡蛋还有 i-1 枚,要查找的楼层还有 x-1 层。


如果鸡蛋没碎,那么 dp[i][j] = dp[i][j-x]+1,也就是鸡蛋还有 i 枚,要查找的楼层还有 j-x 层。


因为我们要考虑最坏的情况,所以可以得到状态转移方程:


dp[i][j] = min(dp[i][j],max(dp[i-1][x-1],dp[i][j-x])+1) (1<=x<=j)


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // 初始化 dp 数组
  dp = Array(k+1);
  for(let i = 0;i<=k;i++){
    dp[i] = Array(n+1).fill(0)
  }
  // 将鸡蛋为 1 的情况赋值对应楼层
  for(let j = 1;j<=n;j++){
    dp[1][j] = j
  }
  // 利用状态转移方程,推导出 dp[k][n]
  for(let i = 2;i<=k;i++){
    for(let j = 1;j<=n;j++){
      dp[i][j] = j;
      for(let x = 1;x<=j;x++){
        dp[i][j] = Math.min(dp[i][j],Math.max(dp[i-1][x-1],dp[i][j-x])+1)
      }
    }
  }
  // 返回结果
  return dp[k][n];
};
复制代码


这样的一版代码虽然逻辑没有问题,基础的测试用例也都可以通过,但是提交会超时,是因为我们求所有的二维 dp 的过程中,利用了双层循环获取所有的鸡蛋和楼层的组合,又进行了楼层次数的遍历获取当前的 dp 值,所以时间复杂度为 O(kn²),空间复杂度为 O(kn)


二分优化


解题思路


接下来,我们思考针对上一版代码,如何进行优化,提高推导 dp 的效率呢?


我们再来重新看一下状态转移方程:


dp[i][j] = min(dp[i][j],max(dp[i-1][x-1],dp[i][j-x])+1)


我们观察 dp[i-1][x-1]dp[i][j-x],前者会根据 x 值增大而增大,后者会根据 x 值增大而减小,同理,前者根据 x 值减小而减小,后者根据 x 值减小而增大。


又因为我们需要取两者的最大值来更新 dp,所以想要取得最优的 dp,就需要让 x 接近于一个中间值,让两个值尽可能的相等,这样它们中的最大值才会尽可能的小。


所以这里我们可以通过二分查找目标 x 值,从而将最内层查找 x 值的时间复杂度从 O(n) 降低到 O(logn),整体时间复杂度为 O(knlogn),空间复杂度为 O(kn)


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // 初始化 dp 数组
  dp = Array(k+1);
  for(let i = 0;i<=k;i++){
    dp[i] = Array(n+1).fill(0)
  }
  // 将鸡蛋为 1 的情况赋值对应楼层
  for(let j = 1;j<=n;j++){
    dp[1][j] = j
  }
  // 利用状态转移方程,推导出 dp[k][n]
  for(let i = 2;i<=k;i++){
    for(let j = 1;j<=n;j++){
      dp[i][j] = j;
      // 利用二分查找最优 x 值
      let l = 1,r = j;
      while(l<r){
        const mid = (l+r)>>1;
        if(dp[i-1][mid-1]<dp[i][j-mid]) l = mid+1
        else r = mid
      }
      dp[i][j] = Math.min(dp[i][j],Math.max(dp[i-1][l-1],dp[i][j-l])+1)
    }
  }
  // 返回结果
  return dp[k][n];
};
复制代码


这一次提交是可以通过的,但是结果依然不理想,执行用时在 300ms 左右。


记忆 x 值


解题思路


接下来我们思考针对上一版代码,还能如何进行优化呢?


我们思考这样一个问题,在我们的内层循环中(1~n)j 是单调递增的,那么此时最优的 x 值必然也是单调递增的。所以每一次 j 增大后,我们只需要在上次的 x 值基础上向后查找最优的 x 值即可。这样就把时间复杂度优化到了 O(kn)


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // 初始化 dp 数组
  dp = Array(k+1);
  for(let i = 0;i<=k;i++){
    dp[i] = Array(n+1).fill(0)
  }
  // 将鸡蛋为 1 的情况赋值对应楼层
  for(let j = 1;j<=n;j++){
    dp[1][j] = j
  }
  // 利用状态转移方程,推导出 dp[k][n]
  for(let i = 2;i<=k;i++){
    // 通过记忆 x 值优化推导过程
    let x = 1;
    for(let j = 1;j<=n;j++){
      dp[i][j] = j;
      while(dp[i-1][x-1]<dp[i][j-x]) x++
      dp[i][j] = Math.min(dp[i][j],Math.max(dp[i-1][x-1],dp[i][j-x])+1)
    }
  }
  // 返回结果
  return dp[k][n];
};
复制代码


这一版代码提交,效率比之前有明显提升,执行用时在 150ms 左右。但是整体结果依然不理想,只击败了 30% 左右的用户。


记忆 x 值 + 滚动数组


解题思路


接下来我们还可以优化一下我们的代码。


我们观察状态转移方程:


dp[i][j] = min(dp[i][j],max(dp[i-1][x-1],dp[i][j-x])+1)


可以看到,每次的 dp[i][j] 只依赖于当前所处数组以及 i-1 数组,所以我们可以通过滚动数组来优化空间复杂度,从之前的 O(kn) 优化到 O(n)


滚动数组


这里我们说一下滚动数组技巧。


因为数组下标是奇偶变化的,所以这里我们对当前下标 i%2,只会得到 0,1 两种结果,而 i-1 对应的结果则刚好是当前下标对应的 0,1 中的另一个值,这样就形成了 i-1 存储在 0,i 存储在 1,i+1 存储在 0 ...


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // 初始化 dp 数组
  dp = Array(2);
  for(let i = 0;i<2;i++){
    dp[i] = Array(n+1).fill(0)
  }
  // 将鸡蛋为 1 的情况赋值对应楼层
  for(let j = 1;j<=n;j++){
    dp[1][j] = j
  }
  // 利用状态转移方程,推导出 dp[k][n]
  for(let i = 2;i<=k;i++){
    // 通过滚动数组优化空间复杂度
    let cur = i%2,pre = !cur*1;
    // 通过记忆 x 值优化推导过程
    let x = 1;
    for(let j = 1;j<=n;j++){
      dp[cur][j] = j;
      while(dp[pre][x-1]<dp[cur][j-x]) x++
      dp[cur][j] = Math.min(dp[cur][j],Math.max(dp[pre][x-1],dp[cur][j-x])+1)
    }
  }
  // 返回结果
  return dp[k%2][n];
};
复制代码


代码提交后,在内存消耗方面击败了 80+% 的用户。


倒推版动态规划


解题思路


针对上述的状态定义,第四版代码基本已经把能优化的点都做了,但是依然无法达到最优解。


所以这里我们尝试另一种状态定义解决本题。


这里我们定义二维 dp => dp[i][j],其中 i 表示操作次数,j 表示鸡蛋数量,dp[i][j] 表示操作 i 次,拥有 j 枚鸡蛋,所能测出的最高楼层。


这一种状态定义之所以更高效是因为之前的 dp[鸡蛋数量][楼层高度],而现在是 [操作次数][鸡蛋数量],而通过之前的解题过程我们可以知道,操作次数和楼层高度之间是指数级关系的,所以我们这里推导二维 dp 的时候,要比之前循环的次数少很多。


推导转移方程和之前类似,同样只有两种情况,鸡蛋碎以及鸡蛋没碎。


假设当前在第 x 层扔鸡蛋,


如果鸡蛋碎了,说明 x 层到第 n 层都是大于 f 的,所以可以确定 dp[i-1][j-1]<=f<x


可以发现,这种情况是比较好的,不管楼层有多高,通过这一次,我们都把范围确定在了 dp[i-1][j-1]~x


如果没碎,那么这个鸡蛋我们还可以继续使用。


此时有 i-1 次操作机会,j 个鸡蛋,所有还可以查找 dp[i-1][j] 个楼层,


所以这个时候我们 dp[i][j] 可以求解 dp[i-1][j-1]+1+dp[i-1][j] 个楼层。


这里的 1 是我们当前扔鸡蛋的这一层楼。


因为第一种情况虽好,但是是可遇不可求的,如果没有发生第一种情况,则第二种情况是必然的。


所以这里我们最差的情况就是每次都是第二种情况,为了一定能求得结果,所以这里采用第二种情况的转移方程。


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // 初始化 dp 数组
  dp = [Array(k+1).fill(0)];
  // 初始化操作次数
  let cnt = 0;
  // 当 dp[cnt][k] 小于目标楼层的时候,递增操作次数,直到可以测出目标楼层
  while(dp[cnt][k]<n){
    // 递增操作次数
    cnt++
    // 初始化 dp[cnt]
    dp[cnt] = [0]
    // 推导 dp
    for(let j = 1;j<=k;j++){
      dp[cnt][j] = dp[cnt-1][j-1]+1+dp[cnt-1][j]
    }
  }
  // 返回操作次数
  return cnt;
};
复制代码


num 表示最终操作次数,此时时间复杂度为 O(numK),空间复杂度为 O(numK)


又因为 numN 之间是指数级的关系,所以本题解的时间复杂度为 O(KlogN),空间复杂度为 O(KlogN)


提交后执行用时和内存消耗都击败了 80+% 的用户,已经非常接近最优解了。


一维 DP 优化


解题思路


因为推导 dp 的过程中,本次的值只依赖于上一次操作次数,所以可以将 dp 数组优化为一维 dp


dp[i] 表示当前次数下 i 个鸡蛋可以测出的最高楼层。


因为下一次依赖上一次的值,所以应该是从后向前推每一层的值,如此一层一层的更新 dp


代码实现


var superEggDrop = (k, n)=> {
  // 特判,如果只有一枚鸡蛋,返回楼层数
  if(k===1) return n;
  // / 初始化 dp 数组
  const dp = Array(k+1).fill(0);
  // 初始化操作次数
  let cnt = 0;
  // 当 dp[cnt][k] 小于目标楼层的时候,递增操作次数,直到可以测出目标楼层
  // 要注意的是这里的 cnt 没没有表现在 dp 数组中,但是是切实存在的
  while(dp[k]<n){
    // 递增操作次数
    cnt++;
    // 推导 dp
    for(let j = k;j>0;j--){
      dp[j] = dp[j-1]+1+dp[j]
    }
  }
  // 返回操作次数
  return cnt;
};
复制代码


本题解的时间复杂度和上一版相同,为 O(KlogN),空间复杂度为 O(K)


提交后执行用时和内存消耗都击败了 90+% 的用户,达到了最优解。


至此我们就完成了 leetcode-887-鸡蛋掉落


如有任何问题或建议,欢迎留言讨论!

相关文章
|
6月前
|
Go
golang力扣leetcode 699.掉落的方块
golang力扣leetcode 699.掉落的方块
46 0
|
机器学习/深度学习 算法
[leetcode] 鸡蛋掉落 Google面试题 dp
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。 已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。 每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。 请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
227 0
[leetcode] 鸡蛋掉落 Google面试题 dp
|
2月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
54 6
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 26. 树的子结构
这篇文章提供了解决LeetCode上"剑指Offer 26. 树的子结构"问题的Python代码实现和解析,判断一棵树B是否是另一棵树A的子结构。
46 4
|
3月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
107 2
|
9天前
|
机器学习/深度学习 人工智能 自然语言处理
280页PDF,全方位评估OpenAI o1,Leetcode刷题准确率竟这么高
【10月更文挑战第24天】近年来,OpenAI的o1模型在大型语言模型(LLMs)中脱颖而出,展现出卓越的推理能力和知识整合能力。基于Transformer架构,o1模型采用了链式思维和强化学习等先进技术,显著提升了其在编程竞赛、医学影像报告生成、数学问题解决、自然语言推理和芯片设计等领域的表现。本文将全面评估o1模型的性能及其对AI研究和应用的潜在影响。
10 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
3月前
|
索引 Python
【Leetcode刷题Python】从列表list中创建一颗二叉树
本文介绍了如何使用Python递归函数从列表中创建二叉树,其中每个节点的左右子节点索引分别是当前节点索引的2倍加1和2倍加2。
54 7
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 30. 包含min函数的栈
本文提供了实现一个包含min函数的栈的Python代码,确保min、push和pop操作的时间复杂度为O(1)。
25 4