惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Announcements
Recent Announcements
Y
Y Combinator Blog
Vercel News
Vercel News
Martin Fowler
Martin Fowler
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LangChain Blog
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
The GitHub Blog
The GitHub Blog
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
阮一峰的网络日志
阮一峰的网络日志
F
Fortinet All Blogs

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog 利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog 状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 Kotlin 的那些骚操作 | Kenvix's Blog 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 计算机幻觉从入门到入土 | Kenvix's Blog Java 注解预处理 Annotation Processing & 代码生成 Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog
扔鸡蛋问题 | Kenvix's Blog
2019-02-14 · via Kenvix's Blog

img 菜鸡第一次看算法题,这篇笔记还是不要看比较好

题目

x星球的居民脾气不太好,但好在他们生气的时候唯一的异常举动是:摔手机。
各大厂商也就纷纷推出各种耐摔型手机。x星球的质监局规定了手机必须经过耐摔测试,并且评定出一个耐摔指数来,之后才允许上市流通。

x星球有很多高耸入云的高塔,刚好可以用来做耐摔测试。塔的每一层高度都是一样的,与地球上稍有不同的是,他们的第一层不是地面,而是相当于我们的2楼。

如果手机从第7层扔下去没摔坏,但第8层摔坏了,则手机耐摔指数=7。
特别地,如果手机从第1层扔下去就坏了,则耐摔指数=0。
如果到了塔的最高层第n层扔没摔坏,则耐摔指数=n

为了减少测试次数,从每个厂家抽样3部手机参加测试。

某次测试的塔高为1000层,如果我们总是采用最佳策略,在最坏的运气下最多需要测试多少次才能确定手机的耐摔指数呢?

解决方案

这东西就是传统的扔鸡蛋问题。

二分法

通常使用此方法的前提是 鸡蛋个数 ≥ ⌈log2M⌉ (M表示楼层个数)例如,9~16层楼在最坏情况下都需要多达 4 个鸡蛋才能得出结果。

倘若鸡蛋个数少,当只剩最后一个鸡蛋时,只能最保守地在一个较大的区间内逐层向上扔鸡蛋,显然会徒增尝试次数,不宜使用。

动态规划

动态规划思想是利用对已求解的重复子问题进行记忆化存储,然后避免重复计算,利用最优化原理得出结果。说人话就是粗暴地递归找。

此方法需要寻找“状态转移方程式”,并依此“倒推”求解

先设一个函数 f(m,n) m 表示楼层数(不是楼高),n 表示鸡蛋数,返回值为最优解在最恶劣情况下需要尝试的次数。

假设第一个鸡蛋在 x 层 (1≤x≤m) 扔出,将出现两种情况:

  1. 鸡蛋在 x 层抛出后碎了
  2. 鸡蛋在 x 层抛出后没碎

鸡蛋碎了

显然接下来应从 (1≤y≤x-1) 层扔出鸡蛋,由于鸡蛋碎了,鸡蛋个数将 -1,同时尝试过一次了,所以尝试次数 +1

根据上面所设函数,可以将这种情况用同样的函数表示为 f(x-1, n-1) +1

鸡蛋没碎

由于鸡蛋没有碎,可以确保小于等于此层数的都没有问题,并且鸡蛋个数没有减少
剩余待测试层数为 m-x,可以用函数表示为 f(m-x, n) +1

这个函数描述在剩下 m-x 个楼层,n 个鸡蛋,返回值为最优解在最恶劣情况下需要尝试的次数。

为什么是 m-x ?

如果在思考这里的时候,代入的一个 x 值比较大,则容易在这里面绕进去。

受平时写业务代码的影响,此处的 f(m-x, n) 易被理解为具有判断鸡蛋是否摔碎的功能,但之前定义 f(a,b) 只是一个用于计算摔鸡蛋所需次数的函数,并非处理业务逻辑的函数,即并不是判断鸡蛋会不会摔碎的函数。

也就是说,尽管已经知道 在 m-x 层的鸡蛋并不会碎(业务逻辑),但仍需假设鸡蛋会碎,并求出在 m-x 层、有 n 个鸡蛋时需要尝试多少次。

f 的第一个参数压根就不是表示楼高的,而是楼层的个数!个数!

得出公式

在从 x 层扔鸡蛋的条件下,此处尚不知道到底鸡蛋碎还是不碎是更加恶劣的情况(需要进一步尝试的次数多),因此需要同时求出上述两种情况,并取最大者:

f(m,n)=max{ f(x-1, n-1)+1, f(m-x, n)+1 }

题设要求我们解出哪层(x)扔鸡蛋可以在最恶劣的情况下(上式)所得值最小,因此需要遍历x (1≤x≤m),分别代入 1≤x≤m 求出值,取最小者,函数返回值即为最优解在最恶劣情况下需要尝试的次数,而使这个返回值成立的x即为所求x(即所求最优解)

综上,易得公式:

f(m,n)=min{ max{f(1-1, n-1)+1, f(m-1, n)+1}, max{f(2-1, n-1)+1, f(m-2, n)+1}, max{f(3-1, n-1)+1, f(m-3, n)+1}, ..., max{f(m-1, n-1)+1, f(m-m, n)+1} }

代码实现

以从 100 楼下落,持有 2 个鸡蛋为例

public class Main {
    private static final int Floor = 100;
    private static final int Egg = 2;

    private static int f(final int m, final int n) {
        int min = 0;
        boolean isInitialized = false;

        if (m == 0)
            return 0;

        for (int x = 1; x <= m; x++) {
            int result = Math.max(f(x-1, n-1)+1, f(m-x, n)+1);

            if(!isInitialized || min > result) {
                isInitialized = true;
                min = result;
            }
        }

        System.out.printf("progress: m=%d value=%d \n", m, min);
        return min;
    }

    public static void main(String[] args) {
        System.out.println("result: " + f(Floor, Egg));
    }
}

跑一下就可以发现,计算花费了大量的时间,这是由于递归的资源开销很大,时间复杂度大,因此效率较低,需要在上式的基础上进行优化。

不优化可以吗?原题 1000 层楼,3 个鸡蛋,我觉得跑到收卷也跑不完,更何况还是渣机。

简单的优化

尝试重新整理思路以改写成循环的形式。

首先绘制一张表格:

持有的鸡蛋数(n)/楼层数(m) 0层楼 1层楼 2层楼 3层楼 4层楼
0个鸡蛋
1个鸡蛋
2个鸡蛋
3个鸡蛋

显然:

  1. 在只有一个鸡蛋时,楼层数=尝试次数
  2. 在只有一层楼时,不管多少个鸡蛋都只尝试一次
  3. 0 层楼或 0 个鸡蛋时不需要尝试,尝试次数记为 0
持有的鸡蛋数(n)/楼层数(m) 0层楼 1层楼 2层楼 3层楼 4层楼
0个鸡蛋 0 0 0 0 0
1个鸡蛋 0 1 2 3 4
2个鸡蛋 0 1
3个鸡蛋 0 1

使用上面的公式,算出2个鸡蛋时、不同层楼的情况,再算出3个鸡蛋时、不同层楼的情况。

求解过程略,但可以发现:求解需要用到上次求解的结果,例如,在求解2个鸡蛋2层楼时,需要用到0层楼2个鸡蛋、1层楼1个鸡蛋的结果。显然,这个表格的填充顺序应是从上到下、从左到右、填完一行到下一行。

根据公式 f(m,n)=max{ f(x-1, n-1)+1, f(m-x, n)+1 }
得出:表格坐标(m,n)的值为 max{(x-1, n-1)的值+1, (m-x, n)的值+1}

持有的鸡蛋数(n)/楼层数(m) 0层楼 1层楼 2层楼 3层楼 4层楼
0个鸡蛋 0 0 0 0 0
1个鸡蛋 0 1 2 3 4
2个鸡蛋 0 1 2 2 3
3个鸡蛋 0 1 2 2 3

整理思路

整理一下上面的操作,可以分为下面的过程:

  1. 建立一个二维数组模仿上面的表格保存结果
  2. 对只有一个鸡蛋时直接返回楼层数,暂时将其他填充为最大尝试次数
  3. 2个以上的鸡蛋时,首先枚举鸡蛋个数(n)
  4. 枚举鸡蛋个数(n)时,枚举楼层数(m)
  5. 枚举楼层数(m)时,枚举从哪层楼开始扔(x)
  6. 枚举从哪层楼开始扔(x)时,算出这层楼的尝试次数
  7. 对应“坐标”的值为尝试次数的最小者
  8. 最终所求解为表格最右下角的那个值

写成代码:

import java.util.stream.IntStream;

public class Main {
    private static final int Floor = 1000;
    private static final int Egg = 3;

    private static int getMinDropEggTries(final int floorNum, final int eggNum) {
        if (floorNum < 1 || eggNum < 1)
            return 0;

        int[][] result = new int[eggNum+1][floorNum+1]; //坐标为 (鸡蛋,楼层)

        IntStream.range(1, floorNum+1).forEach(value -> result[1][value] = value);
        IntStream.range(1, eggNum+1).forEach(value -> result[value][1] = 1);

        IntStream.range(2, eggNum+1).forEach(egg ->
                IntStream.range(2, floorNum+1).forEach(floor ->
                        IntStream.range(1, floor+1).forEach(x -> {
                                    int value = Math.max(result[egg-1][x-1] + 1, result[egg][floor-x] + 1);

                                    if(result[egg][floor] == 0 || result[egg][floor] > value)
                                        result[egg][floor] = value;
                                })));

        return result[eggNum][floorNum];
    }

    public static void main(String[] args) {
        System.out.println("result: " + getMinDropEggTries(Floor, Egg));
    }
}

进一步优化

不难发现,int[][] result 实际上在后续计算中并没有使用,可以将其简化以降低空间复杂度。