动态规划解决鸡蛋掉落问题

news2024/9/25 17:20:09

目录

问题描述

解决问题

①蛮力法

②备忘录

③递归改递推

④二分法

⑤逆向思维


问题描述

给定一定楼层数的建筑物和一定数量的鸡蛋,求出可以找出门槛楼层的最少鸡蛋掉落实验的次数,约束条件是:幸存的鸡蛋可以重复使用,破碎的鸡蛋不能再次使用,如果鸡蛋从此层掉落会碎,那么从更高的楼层掉落也会碎,如果鸡蛋从此层掉落不会碎,那么从更低的楼层掉落也不会碎。

解决问题

我们先来看只有一个鸡蛋的情况,如图1所示,由于鸡蛋会破碎不可再次使用,所以我们只能从最低的楼层开始扔鸡蛋,如果在第一层楼扔鸡蛋没有碎,那么继续往上也就是第二层扔鸡蛋,如果鸡蛋在某一层扔碎了,那么说明门槛楼层就是这一层。这样要找出门槛楼层的最少扔鸡蛋的次数就是楼层的层数。

图1 只有一个鸡蛋的情况

那如果我们有足够多的鸡蛋,即鸡蛋数大于等于楼层数,如图2所示,我们可以先随便在一个楼层扔一个鸡蛋看看会发生什么。

图2 有足够多的鸡蛋

比如说我们选择在3楼扔出一个鸡蛋,如图3所示。

图3 在3楼扔出一个鸡蛋

那么事实是扔出的鸡蛋会有两种结果,一种情况是鸡蛋在3楼扔下破碎了,那么说明我们要找的门槛楼层在3楼以下的楼层,如图4所示,同时我们可以用的鸡蛋减少了一个,即问题的规模减少。

图4 鸡蛋破碎的情况

还有一种情况是鸡蛋从3楼扔下去没碎,那么我们刚刚扔下去的鸡蛋还可以再重复使用,也就是可用的鸡蛋数没有减少,同时说明我们要找的门槛楼层在3楼以上,如图5所示,这样问题的规模也减小了。

图5 鸡蛋没碎的情况

我们记Time[egg][height]为egg个鸡蛋和height高的楼层要找出门槛楼层所需要的扔鸡蛋的次数,通过刚刚的分析,我们知道要求解Time[egg][height]可以先在high层扔一次鸡蛋,那么Time[egg][height]的值就应该为Time[egg-1] [high-1](鸡蛋碎了,鸡蛋数减少一个,楼层数减少一层)和Time[egg][height-high](鸡蛋没碎,鸡蛋数没变,楼层往上)的较大值加一(因为我们扔了一次鸡蛋),所以可以得出下面的状态转移方程:

Time[egg][height]=1+max (Time[egg-1] [high-1], Time[egg][height-high])

通过上面的分析,我们可以知道,要求解Time[egg][height]可以通过求解其子问题Time[egg-1] [high-1]和Time[egg][height-high]来实现。

因为我们要找出最少的扔鸡蛋的次数,所以要在所有楼层扔鸡蛋的情况中寻找最小值,更准确的状态转移方程应该是下面这个:

①蛮力法

蛮力法即直接枚举所有楼层扔鸡蛋的情况,如图6所示,把在每一层楼扔鸡蛋的情况都计算一遍,找出其中的最小值。

图6 蛮力法枚举所有情况

采用纯递归的方法,可以对所有可能的楼层进行遍历,计算在这一层扔鸡蛋所需的最小尝试次数。具体地,对于第i层楼,需要递归调用函数,在第i层扔鸡蛋和在第i层楼上方继续搜索两种情况下,所需的尝试次数取二者中的最大者,再加上一次实际的尝试机会,即可得到在第i层楼扔鸡蛋的最小尝试次数。

纯递归的时间复杂度非常高,随楼层数的增加指数增长,随鸡蛋数的增加线性增长。

C++代码

//
// Created by YEZI on 2023/5/15.
//

#ifndef EGGDROP_BRUTE_H
#define EGGDROP_BRUTE_H
#include<iostream>
namespace brute{
    int superEggDrop(int egg,int height){
        if(egg==1||height<=2)
            return height;
        int minTimes=height;
        for(int i=1;i<=height;i++){
            minTimes=std::min(minTimes,std::max(superEggDrop(egg,i-1), superEggDrop(egg-1,height-i))+1);
        }
        return minTimes;
    }
}
#endif //EGGDROP_BRUTE_H

测试

我们先在LeetCode上提交代码进行测试,测试结果如图7所示,可见蛮力法通过了32个测试样例,验证了算法的正确性,但是在鸡蛋数为3、楼层数为26的时候超出了时间的限制。

图7 LeetCode蛮力法测试

我们固定楼层数为20层,测试不同鸡蛋数的结果如图8所示,符合线性增长的预测。

图8 蛮力法 固定楼层数

具体数据如表1所示。

表1 蛮力法 固定楼层数

固定鸡蛋的个数为10个,测试不同楼层的结果如图9所示,符合指数增长。

图9 蛮力法 固定鸡蛋数

具体数据如表2所示。

表2 蛮力法 固定鸡蛋数

结果分析

由结果可知,纯递归的暴力枚举的执行效率比较差,速度非常慢,由于没有将每个子问题的解记录下来,每次都需要重新计算子问题的解,重复计算大大增加,加上递归调用函数的开销也很大,其计算的时间效率随楼层数的增加呈指数增长,随鸡蛋数的增加线性增长,

因此我们需要对算法进行优化。

②备忘录

纯递归的暴力枚举重复计算了子问题的解,因此我们可以开辟二维数组将计算过程的子问题的解记录下来,这样将不再需要重新计算子问题的解,时间复杂度为O(egg*height^2),空间复杂度为O(egg*height)。

C++代码 

//
// Created by YEZI on 2023/5/15.
//

#ifndef EGGDROP_DYNAMICPROGRAM_H
#define EGGDROP_DYNAMICPROGRAM_H
#include<iostream>
namespace dynamicProgram{
    int **dp;
    int Drop(int egg,int height){
        if(dp[egg][height]!=-1){
            return dp[egg][height];
        }
        if(egg==1||height<=2){
            dp[egg][height]=height;
            return height;
        }
        int minTimes=height;
        for(int i=1;i<=height;i++){
            minTimes=std::min(minTimes,std::max(Drop(egg,i-1), Drop(egg-1,height-i))+1);
        }
        dp[egg][height]=minTimes;
        return minTimes;
    }

    int superEggDrop(int egg,int height){
        dp=new int*[egg+1];
        for(int i=1;i<=egg;i++){
            dp[i]=new int[height+1];
        }
        for(int i=1;i<=egg;i++){
            for(int j=0;j<=height;j++){
                dp[i][j]=-1;
            }
        }
        return Drop(egg,height);
    }
}
#endif //EGGDROP_DYNAMICPROGRAM_H

测试

我们先在LeetCode上提交代码进行测试,测试结果如图10所示,可见备忘录法通过了67个测试样例,比先前的通过个数更多了,验证了算法的正确性,但是在鸡蛋数为5、楼层数为2000的时候超出了时间的限制。

图10 LeetCode备忘录测试

固定楼层数为500层,测试不同鸡蛋个数的结果如图11所示,符合线性增长。

图11 备忘录 固定楼层数

具体数据如表3所示。

表3 备忘录 固定楼层数

固定鸡蛋的个数为10个,测试不同楼层的结果如图12所示,符合平方增长。

图12 备忘录 固定鸡蛋数

具体数据如表4所示。

表4 备忘录 固定鸡蛋数

结果分析

由结果可以看出,与之前的暴力枚举相比,我们将每个子问题的解记录下来的优化效果十分明显,可以测试的数据规模明显增长,但是我们仍然没有在规定时间内通过LeetCode上的所有测试用例,我们还需要继续优化算法。

③递归改递推

我们先前两个策略都是采用递归调用函数实现的,反复递归调用函数的开销很大,因此我们在备忘录的基础上将递归调用函数改为循环内递推,时间复杂度和空间复杂度和备忘录相比没有变化,但理论上递推执行起来会更快。

C++代码

//
// Created by YEZI on 2023/5/21.
//

#ifndef MAIN_CPP_ITERATIVEDP_H
#define MAIN_CPP_ITERATIVEDP_H
#include<iostream>
namespace iterativeDP{
    int superEggDrop(int egg,int height){
        int **dp=new int*[egg+1];
        for(int i=1;i<=egg;i++){
            dp[i]=new int[height+1];
            dp[i][0]=0;
            dp[i][1]=1;
        }
        for(int i=1;i<=height;i++){
            dp[1][i]=i;
        }
        for(int i=2;i<=egg;i++){
            for(int j=2;j<=height;j++){
                dp[i][j]=height;
                for(int k=1;k<=j;k++){
                    dp[i][j]=std::min(dp[i][j],std::max(dp[i-1][k-1],dp[i][j-k])+1);
                }
            }
        }
        return dp[egg][height];
    }
}
#endif //MAIN_CPP_ITERATIVEDP_H

测试

我们先在LeetCode上提交代码进行测试,测试结果如图13所示,可见递推法通过了74个测试样例,与之前的相比通过的个数更多了,验证了算法的正确性,但是在鸡蛋数为6、楼层数为10000的时候还是超出了时间的限制。

图13 LeetCode 递推测试

固定楼层数为500层,测试不同鸡蛋个数的结果如图14所示,符合线性增长的预测。

图14 递推固定楼层数

具体数据如表5所示。

表5 递推固定楼层数

固定鸡蛋个数为10个,测试不同楼层数的结果如图15所示。

图15 递推固定鸡蛋数

具体数据如表6所示。

表6 递推固定鸡蛋数

结果分析

由结果可以看出来,递推法相比备忘录的速度更快了,但是还是没有在规定时间内通过LeetCode上的所有测试用例,我们还得继续努力。

④二分法

先前我们是暴力枚举了所有楼层扔鸡蛋的情况去找最小的测试次数,但事实上我们可能并不需要把每一个情况都计算一次。我们通过求解子问题Time[egg-1][high-1](鸡蛋破碎的情况,可用的鸡蛋个数减一,楼层数减一)和子问题Time[egg][height-high](鸡蛋没碎的情况,楼层数减少),我们要求二者的较小值,而从数学上,Time[egg-1][high-1]是high单调递增的函数,Time[egg][height-high]是high单调递减的函数,如图16所示,我们可以通过二分法找到满足要求的high值,从而将把时间复杂度从原本的O(egg*height^2)缩小为O(egg*height*log(height))。

图16 二分法

C++代码

//
// Created by YEZI on 2023/5/21.
//

#ifndef MAIN_CPP_DICHOTOMY_H
#define MAIN_CPP_DICHOTOMY_H

#include<iostream>

namespace dichotomy {
    int superEggDrop(int egg, int height) {
        int **dp = new int *[egg + 1];
        for (int i = 1; i <= egg; i++) {
            dp[i] = new int[height + 1];
            dp[i][0] = 0;
            dp[i][1] = 1;
        }
        for (int i = 1; i <= height; i++) {
            dp[1][i] = i;
        }
        for (int i = 2; i <= egg; i++) {
            for (int j = 2; j <= height; j++) {
                int low = 1;
                int high = j;
                while (low < high) {
                    int mid = (low + high + 1) / 2;
                    if (dp[i - 1][mid - 1] <= dp[i][j - mid]) {
                        low = mid;
                    } else {
                        high = mid - 1;
                    }
                }
                dp[i][j] = std::max(dp[i - 1][low - 1], dp[i][j - low]) + 1;
            }
        }
        return dp[egg][height];
    }
}
#endif //MAIN_CPP_DICHOTOMY_H

测试

我们先在LeetCode上提交代码进行测试,测试结果如图17所示,可见二分法优化的效果非常明显,终于通过了所有的测试用例,耗时324ms,内存消耗25.4MB。

图17 LeetCode 二分法测试

固定楼层数为10000层,测试不同鸡蛋个数的结果如图18所示,符合线性增长的预测。

图18 二分法 固定楼层数

具体数据如表7所示。

表7 二分法 固定楼层数

固定鸡蛋个数为10个,测试不同楼层数的结果如图19所示,符合对数增长的预测。

图19 二分法 固定鸡蛋数

具体数据如表8所示。

表8 二分法 固定鸡蛋数

结果分析

由结果可知,二分优化的效果非常明显,无论是执行速率还是可以处理的数据规模和之前的相比都有非常大的提升。

⑤逆向思维

我们可以将问题反过来想,对于给定time次尝试,egg个鸡蛋,我们可以测出多少层。我们定义high[egg]为egg个鸡蛋可以测出的层数,那么每一次尝试中,egg个鸡蛋可以测试出的层数就应该等于上一次尝试中egg个鸡蛋可以测出的层数加上本次egg个鸡蛋可能测出的层数,即:

high[egg]=1+high[egg]+high[egg-1]

而对于要解决鸡蛋掉落问题,我们只需要一次一次的尝试,直到high[egg]大于等于所给定的楼层数即可。

这个算法的空间复杂度为O(egg),时间复杂度为O(egg*height),事实上,更确切的时间复杂度为

C++代码 

//
// Created by YEZI on 2023/5/21.
//

#ifndef MAIN_CPP_BACKWARD_H
#define MAIN_CPP_BACKWARD_H
namespace backward{
    int superEggDrop(int egg,int height){
        int*high=new int[egg+1];
        for(int i=0;i<=egg;i++){
            high[i]=0;
        }
        int times=0;
        while(high[egg]<height){
            times++;
            for(int i=egg;i>0;i--){
                high[i]=high[i]+high[i-1]+1;
            }
        }
        return times;
    }
}
#endif //MAIN_CPP_BACKWARD_H

测试

我们先在LeetCode上提交代码进行测试,测试结果如图20所示,逆向思维方法通过了所有的测试用例,而且更快,耗时0ms,内存消耗5.9MB。

图20 LeetCode逆向思维测试

固定楼层数为一百万层,测试不同鸡蛋个数的结果如图21所示,符合线性增长的预测。

图21 逆向思维法 固定楼层数

具体数据如表9所示。

表9 逆向思维法 固定楼层数

固定鸡蛋数为1000000个,测试不同楼层数的结果如图22所示,符合预测。

图22 逆向思维法 固定鸡蛋个数

具体数据如表10所示。

表10 逆向思维法 固定鸡蛋个数

结果分析

由结果可以看出,该优化策略效果非常明显,运行速度非常之快,能够处理的规模非常大,百万级别的数据也可以在毫秒级别解决。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/731564.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多模态系列论文--BEiT-3 详细解析

论文地址&#xff1a;Image as a Foreign Language: BEIT Pretraining for All Vision and Vision-Language Tasks 论文代码&#xff1a;BEiT-3 BEiT-3 1 引言&#xff1a;Big Convergence&#xff08;大一统&#xff09;2 BEIT-3预训练框架3 下游任务实现框架4 实验效果5 总结…

李沐动手学深度学习:softmax回归的从零开始实现

import torch from IPython import display from d2l import torch as d2lbatach_size256 train_iter,test_iter d2l.load_data_fashion_mnist(batach_size) num_input 784 #图片的尺寸&#xff1a;28*28 num_output 10 #10个类别 W torch.normal(0,0.01,size(num_input,nu…

Docker尝鲜

一、Docker的安装 卸载系统自带的旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc 获取软件最新源 sudo apt-get update 安装apt依赖包 sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common 安装…

Feign实现远程接口的调用

Feign实现远程接口的调用 前言一、Fegin是什么&#xff1f;二、Feign使用步骤准备工作被调用的远程服务必须上传到nacos/eureka服务中心进行管理配置&#xff0c;比如我调用media-api服务(媒资管理服务)&#xff0c;那么media-api必须被nacos/eureka所管理。如图&#xff0c;都…

CentOS7和主机Win11不能复制粘贴解决之道及CentOS7最小安装版 VMware Tools安装

入世心法&#xff1a; 金字塔 结构化 系统化 底层认知 认知升级 来认识下你老祖宗 底层逻辑 分别从: ---->道|法|术|器|势<--- 这五个层次去打通............................翻新思维底层认知&#xff…

【CSS】跳动文字

文章目录 效果展示代码实现 效果展示 代码实现 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>一颗不甘坠落的流星</title></head><style type"text/css">/* 遮罩盒子样式 */#mask {/* 设…

基于单片机的蓝牙音乐喷泉的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;通过HM-18蓝牙音频模块进行无线传输&#xff1b; 通过LM386功放模块对音频信号进行放大&#xff1b;手机端可以直接控制音频播放&#xff0c;并且最远距离可达20米&#xff1b;手机端可以进行任意音乐切换&#xff0c;播报、暂停&a…

Python 图像文件压缩,使用PIL库对图像进行等比例压缩

题目 图像文件压缩。使用PIL库对图像进行等比例压缩&#xff0c;无论压缩前文件大小如何&#xff0c;压缩后文件大小小于10KB。 代码 from PIL import Image import os from tkinter import filedialog import tkinterf_path filedialog.askopenfilename() image Image.op…

第三章:L2JMobius学习 – 使用eclipse2023创建java工程

在前两个章节中&#xff0c;我们已经安装了mariadb数据库和jdk&#xff0c;本章节我们安装eclipse2023。eclipse作为老牌的java开发工具&#xff0c;真的是不错的。官方下载地址为&#xff1a; https://www.eclipse.org/downloads/download.php?file/technology/epp/download…

STM32的ADC模式及其应用例程介绍

STM32的ADC模式及其应用例程介绍 &#x1f4cd;ST官方相关应用笔记介绍资料&#xff1a;https://www.stmcu.com.cn/Designresource/detail/application_note/705947&#x1f4cc;相关例程资源包&#xff1a;STSW-STM32028&#xff1a;https://www.st.com/zh/embedded-software/…

MySQL---表数据高效率查询(简述)

目录 前言 一、聚合查询 &#x1f496;聚合函数 &#x1f496;GROUP BY子句 &#x1f496;HAVING 二、联合查询 &#x1f496;内连接 &#x1f496;外连接 &#x1f496;自连接 &#x1f496;子查询 &#x1f496;合并查询 &#x1f381;博主介绍&#xff1a;博客名…

NXP文档AN13000解读-基于S32K116的无感BLDC六步换相控制策略(预定位/开环启动/反电动势过零点检测)

目录 六步换相控制 单极性PWM 反电动势过零点检测技术 反电动势的测量 总线电流的测量 电机状态切换 Alignment Start-up Run 算法用到的各模块 各模块间的连接 ADC触发顺序 芯片的初始化 时钟配置与电源管理 FTM Trigger MUX Control (TRGMUX) PDB ADC LPS…

【Git原理与使用】-- 多人协作

目录 多人协作一&#xff08;多人同一分支&#xff09; 开发者一&#xff08;Linux&#xff09; 开发者二&#xff08;Windous&#xff09; master合并 远端上的合并 本地上的合并 总结 多人协作一&#xff08;多人多分支&#xff09; 开发者一&#xff08;Linux&…

SQL Server数据库忘记了sa密码怎么办 亲测真的可用

我们在安装SQL Server数据库时&#xff0c;往往选择混合模式安装&#xff0c;在这里我们可以设置sa密码。混合模式安装后&#xff0c;我们可以通过Windows身份验证和SQL Server身份验证两种方式登陆。 如果sa密码忘记了&#xff0c;我们就无法用SQL Server身份验证登陆了。 那么…

【高性能、高并发】页面静态化解决方案-OpenResty

OpenResty OpenResty介绍 OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台&#xff0c;其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项 用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关 OpenResty通过汇聚各种设计精良的…

Python应用实例(一)外星人入侵(五)

外星人入侵&#xff08;五&#xff09; 1.项目回顾2.创建第一个外星人2.1 创建Alien类2.2 创建Alien实例 3.创建一群外星人3.1 确定一行可容纳多少个外星人3.2 创建一行外星人3.3 重构_create_fleet()3.4 添加行 在游戏《外星人入侵》中添加外星人。我们将首先在屏幕上边缘附近…

RHCE中级项目

一、项目需求 1、在 bbs.example.com 主机上创建 Discuz 论坛&#xff0c;数据库服务器使用 db.example.com 主机的 bbs 数据库实例&#xff0c;该实例由 MySQL数据库软件提供服务。 2、在 ntp.example.com 主机上创建 NTP 服务&#xff0c;该服务由 Chronyd软件提供服务&…

GoLang网络编程:HTTP服务端之底层原理与源码分析——http.HandleFunc()、http.ListenAndServe()

一、启动 http 服务 import ("net/http" ) func main() {http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("ping...ping..."))})http.ListenAndServe(":8999", nil) }在 Golang只需要几行代…

第三章 SSD存储介质:闪存 3.4

3.4 闪存数据完整性 可采用以下数据完整性的技术确保用户数据不丢失&#xff1a; &#xff08;1&#xff09;ECC纠错&#xff1b; &#xff08;2&#xff09;RAID数据恢复&#xff1b; &#xff08;3&#xff09;重读&#xff08;Read Retry&#xff09;&#xff1b; &#xff…

C/C++指针从0到99(详解)

目录 一&#xff0c;指针的基础理解 二&#xff0c;指针的基本使用 三&#xff0c;为什么要用指针 四&#xff0c;指针与数组的联系 五&#xff0c;指针的拓展使用 1&#xff09;指针数组 2)数组指针 3&#xff09;函数指针 结构&#xff1a;返回类型 &#xff08;*p)…