漫画:动态规划解决扔鸡蛋问题

简介: 在上一篇漫画中,小灰介绍了一道有趣的智力题:漫画:有趣的扔鸡蛋问题那么,如何利用动态规划来求出扔鸡蛋问题的通解?换句话说,有M层楼 / N个鸡蛋,要找到鸡蛋摔不碎的临界点,需要尝试几次?

在上一篇漫画中,小灰介绍了一道有趣的智力题:


漫画:有趣的扔鸡蛋问题


那么,如何利用动态规划来求出扔鸡蛋问题的通解?


换句话说,有M层楼 / N个鸡蛋,要找到鸡蛋摔不碎的临界点,需要尝试几次?

image.png


本篇会为大家详细讲述。

image.png

image.png


什么是动态规划?


动态规划英文 Dynamic Programming,是求解决策过程最优化的数学方法,后来沿用到了编程领域。


动态规划的大致思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终得到复杂问题的最优解。


动态规划解决问题的过程分为两步:


1.寻找状态转移方程式

2.利用状态转移方程式自底向上求解问题

image.png

image.png


如何找到状态转移方程式?


在上一篇漫画中,两个鸡蛋100层楼的条件下,我们找到了一个规律:


假设存在最优解,在最坏情况下尝试次数是 X,那么第一个鸡蛋首次扔出的楼层也是 X

image.png



这个规律在三个以上鸡蛋的条件上还能否适用呢?让我们来举个栗子:


假设有三个鸡蛋,100层楼,第一个鸡蛋扔在第10层并摔碎了。这时候我们还剩下两个鸡蛋,因此第二个鸡蛋不必从底向上一层一层扔,而是可以选择在第5层扔。如果第二个鸡蛋也摔碎了,那么第三个鸡蛋才需要老老实实从第1层开始一层一层扔。

image.png


这样一来,总的尝试次数是1+1+4 = 6 < 10。


因此,最优解的最坏情况下尝试次数是 X,鸡蛋首次扔出的楼层也是 X 这个规律不再成立。


那么,我们该怎么寻找规律呢?


我们可以把M层楼 / N个鸡蛋的问题转化成一个函数 F(M,N),其中楼层数M和鸡蛋数N是函数的两个参数,而函数的值则是最优解的最大尝试次数。


假设我们第一个鸡蛋扔出的位置在第X层(1<=X<=M),会出现两种情况:


1.第一个鸡蛋没碎

那么剩余的M-X层楼,剩余N个鸡蛋,可以转变为下面的函数:

 F(M-X,N)+ 1,1<=X<=M


2.第一个鸡蛋碎了

那么只剩下从1层到X-1层楼需要尝试,剩余的鸡蛋数量是N-1,可以转变为下面的函数:

F(X-1,N-1) + 1,1<=X<=M


整体而言,我们要求出的是 M层楼 / N个鸡蛋 条件下,最大尝试次数最小的解,所以这个题目的状态转移方程式如下:


F(M,N)= Min(Max( F(M-X,N)+ 1, F(X-1,N-1) + 1)),1<=X<=M


image.png

image.png



如何进行求解?


状态转移方程式有了,如何计算出这个方程式的结果呢?


诚然,我们可以用递归的方式来实现。但是递归的时间复杂度是指数级的,当M和N的值很大的时候,递归的效率会变得非常低。


根据动态规划的思想,我们可以自底向上来计算出方程式的结果。


何谓自底向上呢?让我们以3个鸡蛋,4层楼的情况为例来进行演示。


请看下面的这张表:

image.png


根据动态规划的状态转移方程式和自底向上的求解思路,我们需要从1个鸡蛋1层楼的最优尝试次数,一步一步推导后续的状态,直到计算出3个鸡蛋4层楼的尝试次数为止。


首先,我们可以填充第一个鸡蛋在各个楼层的尝试次数,以及任意多鸡蛋在1层楼的尝试次数。


原因很简单:

1.只有一个鸡蛋,所以没有任何取巧方法,只能从1层扔到最后一层,尝试次数等于楼层数量。

2.只有一个楼层,无论有几个鸡蛋,也只有一种扔法,尝试次数只可能是1。


image.png


2个鸡蛋2层楼的情况,我们就需要带入状态转移方程式了:


F(2,2) = Min(Max( F(2-X,2)+ 1, F(X-1,2-1) + 1)),1<=X<=2


因为X的取值是1和2,我们需要对X的值逐一来尝试:


当X = 1时,

F(2,2) = Max( F(2-1,2)+ 1, F(1-1,2-1) + 1)) =  Max( F(1,2)+ 1, F(0,1) + 1) = Max(1+1, 0+1) = 2

当X = 2时,

F(2,2) = Max( F(2-2,2)+ 1, F(2-1,2-1) + 1)) =  Max( F(0,2)+ 1, F(1,1) + 1) = Max(0+1, 1+1) = 2


因此,无论第一个鸡蛋先从第1层扔,还是先从第2层扔,结果都是尝试2次

image.png

接下来我们看一看2个鸡蛋3层楼的情况:


F(2,3) = Min(Max( F(3-X,2)+ 1, F(X-1,2-1) + 1)),1<=X<=3

此时X的取值是1,2,3。我们需要对X的值逐一来尝试:


当X = 1时,

F(2,3) = Max( F(3-1,2)+ 1, F(1-1,2-1) + 1)) =  Max( F(2,2)+ 1, F(0,1) + 1) = Max(2+1, 0+1) = 3

当X = 2时,

F(2,3) = Max( F(3-2,2)+ 1, F(2-1,2-1) + 1)) =  Max( F(1,2)+ 1, F(1,1) + 1) = Max(1+1, 1+1) = 2

当X = 3时,

F(2,3) = Max( F(3-3,2)+ 1, F(3-1,2-1) + 1)) =  Max( F(0,2)+ 1, F(2,1) + 1) = Max(1, 2+1) = 3

因此在2个鸡蛋3层楼的情况,最优的方法是第一个鸡蛋在第2层扔,共尝试2次

image.png

依照上面的方式,我们计算出2个鸡蛋4层楼的最优尝试次数,结果是3次。

image.png


同理,我们按照上面的方式,计算出3个鸡蛋在各个楼层的尝试次数,分别是2次,2次,3次。具体计算过程就不再细说。

image.png

image.png

image.png


代码如何实现?


根据刚才的思路,让我们来看一看代码的初步实现:

public class Eggs{

  1. public int getMinSteps(int eggNum, int floorNum){
  2.    if(eggNum < 1 || floorNum < 1) {
  3.        return 0;
  4.    }
  5.    //备忘录,存储eggNum个鸡蛋,floorNum层楼条件下的最优化尝试次数
  6.    int[][] cache = new int[eggNum+1][floorNum+1];
  7.    //把备忘录每个元素初始化成最大的尝试次数
  8.    for(int i=1;i<=eggNum; i++){
  9.        for(int j=1; j<=floorNum; j++)
  10.            cache[i][j] = j;
  11.    }
  12.    for(int n=2; n<=eggNum; n++){
  13.        for(int m=1; m<=floorNum; m++){
  14.            for(int k=1; k<m; k++){
  15.                //扔鸡蛋的楼层从1到m枚举一遍,如果当前算出的尝试次数小于上一次算出的尝试次数,则取代上一次的尝试次数。
  16.                //这里可以打印k的值,从而知道第一个鸡蛋是从第几次扔的。
  17.                cache[n][m] = Math.min(cache[n][m], 1+Math.max(cache[n-1][k-1],cache[n][m-k]));
  18.            }
  19.        }
  20.    }
  21.    return cache[eggNum][floorNum];
  22. }

  23. public static void main(String[] args) {
  24.    Eggs e = new Eggs();
  25.    System.out.println(e.getMinSteps(5,500));
  26. }
  27. }

image.png

image.png

image.png


如何优化呢?


我们从状态转移方程式以及上面的表格可以看出,每一次中间状态的尝试次数,都只和上一层(鸡蛋数量-1)和本层(当前鸡蛋数量)的值有关联:


F(M,N)= Min(Max( F(M-X,N)+ 1, F(X-1,N-1) + 1)),1<=X<=M


image.png


比如我们想要求解3个鸡蛋3层楼的最优尝试次数,并不需要知道1个鸡蛋这一层的值,只需要关心2个鸡蛋和3个鸡蛋在各个楼层的值即可。


这样一来,我们并不需要一个二维数组来存储完整的中间状态记录,只需要利用两个一维数组,存储上一层和本层的尝试次数就足够了。


请看优化版本的代码:

public class EggsOptimized {

  1. public int getMinSteps(int eggNum, int floorNum){
  2.    if(eggNum < 1 || floorNum < 1) {
  3.        return 0;
  4.    }
  5.    //上一层备忘录,存储鸡蛋数量-1的floorNum层楼条件下的最优化尝试次数
  6.    int[] preCache =  new int[floorNum+1];
  7.    //当前备忘录,存储当前鸡蛋数量的floorNum层楼条件下的最优化尝试次数
  8.    int[] currentCache = new int[floorNum+1];
  9.    //把备忘录每个元素初始化成最大的尝试次数
  10.    for(int i=1;i<=floorNum; i++){
  11.        currentCache[i] = i;
  12.    }
  13.    for(int n=2; n<=eggNum; n++){
  14.        //当前备忘录拷贝给上一次备忘录,并重新初始化当前备忘录
  15.        preCache = currentCache.clone();
  16.        for(int i=1;i<=floorNum; i++){
  17.            currentCache[i] = i;
  18.        }
  19.        for(int m=1; m<=floorNum; m++){
  20.            for(int k=1; k<m; k++){
  21.                //扔鸡蛋的楼层从1到m枚举一遍,如果当前算出的尝试次数小于上一次算出的尝试次数,则取代上一次的尝试次数。
  22.                //这里可以打印k的值,从而知道第一个鸡蛋是从第几次扔的。
  23.                currentCache[m] = Math.min(currentCache[m], 1+Math.max(preCache[k-1],currentCache[m-k]));
  24.            }
  25.        }
  26.    }
  27.    return currentCache[floorNum];
  28. }
  29. public static void main(String[] args) {
  30.    EggsOptimized e = new EggsOptimized();
  31.    System.out.println(e.getMinSteps(5,500));
  32. }

}

image.png

image.png


—————END—————

相关文章
|
资源调度 监控 负载均衡
浅析PM2实用入门指南
PM2 是一个守护进程管理器,可以用它来管理你的node进程,负责所有正在运行的进程,并查看node进程的状态,也支持性能监控,负载均衡等功能。使用起来也是非常简单
2344 0
|
关系型数据库 数据库 数据安全/隐私保护
PostgreSQL安装和使用教程
PostgreSQL安装和使用教程
1125 0
|
CDN
静态资源库CDN服务
使用静态资源库可以访问线上资源文件,比如jquery库、bootstrap库。使用百度静态资源库的居多,但是发现百度暂时不支持https协议,bootcdn是一个不错的选择。
3871 0
|
8月前
|
资源调度 监控 测试技术
《SaaS多租户实战指南:从灰度发布到故障容错的全链路架构设计》
本文聚焦企业级团队协作SaaS应用的多租户架构迭代实践,针对租户规模差异大、资源冲突、定制化与标准化矛盾等核心痛点展开。初期简易多租户模式因资源共享导致故障后,作者重构架构:采用“独立数据库+共享数据库+租户标识”的混合隔离方案,解决数据隔离与成本平衡问题;搭建基于租户画像的弹性资源调度体系,通过预测式调度与实时调整提升资源利用率;以“核心标准化+定制插件化”架构,缩短定制需求响应时间;构建分层灰度发布与故障容错机制,将版本故障发生率大幅降低。最终总结出SaaS多租户架构需“以租户为中心”,在隔离、共享、定制间找到精细化平衡点的核心经验。
728 6
|
7月前
|
人工智能 前端开发 安全
AI 最先替代的开发工作:从重复劳动到人机协同的新范式
AI正加速替代基础开发工作:CRUD页面、样板代码、简单Bug修复、文档生成与基础测试等重复性任务已可通过低代码平台与AI工具高效完成,显著提升生产力。据Gartner报告,70%企业内部系统已采用AI辅助开发,人力投入减少60%-80%。GitHub Copilot等工具更让开发者节省45%编码时间。然而,产品需求分析、系统架构设计、复杂交互体验及创新研发等需深度判断与创造力的工作,仍依赖人类智慧。未来开发者将转型为“AI指挥官”,聚焦问题定义、提示工程与人机协同,核心竞争力转向系统思维、业务理解与技术创新。
720 15
|
机器学习/深度学习 人工智能 自然语言处理
与阿里合作的《人工智能(导论)》出版
《人工智能导论——深度学习大模型基础》由赵卫东编著,清华大学出版社出版。本书旨在帮助读者理解深度学习与大模型技术的底层逻辑,通过机器视觉、语音处理及自然语言处理等章节,结合实际应用场景,深入浅出地讲解相关理论。书中引入低代码开发平台和云端实验室资源,助力读者实践所学。无论专业背景如何,本书都能成为进入AI领域的理想入门书籍。特别感谢阿里云及参与编校工作的同学们的支持。
303 0
与阿里合作的《人工智能(导论)》出版
|
JavaScript 前端开发 索引
你可能没有听说过 js中的 DOM操作还有这个: HTMLCollection 和 NodeList
该文章详细解释了JavaScript中HTMLCollection和NodeList这两种DOM集合类型的特性、使用方法及其区别,并通过实例代码展示了如何操作这两种集合来选取和遍历DOM元素。
|
机器学习/深度学习 弹性计算 人工智能
阿里云服务器架构有啥区别?X86计算、Arm、GPU异构、裸金属和高性能计算对比
阿里云ECS涵盖x86、ARM、GPU/FPGA/ASIC、弹性裸金属及高性能计算等多种架构。x86架构采用Intel/AMD处理器,适用于广泛企业级应用;ARM架构低功耗,适合容器与微服务;GPU/FPGA/ASIC专为AI、图形处理设计;弹性裸金属提供物理机性能;高性能计算则针对大规模并行计算优化。
1359 7
|
存储 算法 前端开发
深入理解操作系统:进程调度与优先级队列算法
【9月更文挑战第25天】在操作系统的复杂世界中,进程调度是维持系统稳定运行的核心机制之一。本文将深入探讨进程调度的基本概念,分析不同的进程调度算法,并着重介绍优先级队列算法的原理和实现。通过简洁明了的语言,我们将一起探索如何优化进程调度,提高操作系统的效率和响应速度。无论你是计算机科学的初学者还是希望深化理解的专业人士,这篇文章都将为你提供有价值的见解。
|
算法
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
这篇文章介绍了动态规划算法中解决最大上升子序列问题(LIS)的方法,包括问题的描述、动态规划的步骤、状态表示、递推方程、计算最优值以及优化方法,如非动态规划的二分法。
470 0
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)