每日一题 11.4 鸡蛋掉落

每日一题 11.4 鸡蛋掉落

一、题目概述

这是一道困难题,难度肯定是比前三天的要大的,这个毋庸置疑,但是毕竟趣味性在这里摆着,和昨天那个相比,钻研这个的兴趣要更大一点,昨天那个并查集和哈希表实在是太硬核了,要学不少东西。

不知道这两个重点知识大家还记不记得,我反正是只知道个概念,暂时没尝试代码实现。。。这是极其错误的行为,但是最近确实是复习其他科目了,对这方面不是特别上心,或者说没在这上追求完美。

二、本人思路

本人没什么思路。。。最开始想的是要不要从一楼一点一点扔(暴力枚举法)?但这样肯定比标准答案的数据要大很多。

然后又想到,这个题目,就是把一个数组竖着立起来了,然后这个数组空间里随即存放一个炸弹,鸡蛋放进去,没有炸弹就是没碎,有炸弹就正义捐躯了,可以用二分查找的方法,但又不一定是标准的二分,因为如果有50层楼,但我只有2颗蛋,第一颗扔在25楼,炸了,那我第二颗是扔在12层还是从1层慢慢往上扔啊。

但不管怎么扔,浪费粮食是不对的(doge),大家不要轻易在现实生活中尝试,会被爸妈打的。

扔鸡蛋的次数肯定是大于等于鸡蛋的数量的,这个很容易理解,因为就那么几颗鸡蛋,碎一个就少一个,脸最黑的情况就是全灭,好一点就是一个鸡蛋可以捡起来多用几次。但是这个题还涉及一个0层的问题,就是在1楼扔鸡蛋也会碎的情况,可能有的房子举架高,阳台离地面比较远就碎了。

总结一下,我的大概思路就是,应该是用二分查找,但我又不知道怎么用。

三、大佬思路

这个题的官方题解是给出三种方法,一种是动态规划+二分查找,那看来我答对了1/3,这种是比较基本的,剩下两种是决策单调性和纯数学方法,是竞赛难度的。大家对决策单调性感兴趣的话可以自己翻阅一下官方题解,我没看懂,就不在这里卖弄了。

然后这个题的数学方法是一个比较讨巧的方法,我放在这部分的最后说。

先说动态规划和二分法查找,这两个每个单独拎出来我都会,动态规划,不就多段图和货郎担嘛,二分查找,不就一个算是最简单的查找方式嘛,但是我还真没想过他们的结合体是什么样的。

现在我们已知有 k 个鸡蛋和一个高 n 层的楼房,然后我们设 times ( k, n ) 是最小检测次数,让鸡蛋最不忙碌的一种方法。然后当我们在第 x 层扔下一个鸡蛋的时候,只有两种情况:蛋碎了和没碎。要是碎了,那现在我们要考虑的问题就是 times ( k-1, x-1 ) ,我们少了一个鸡蛋,但是我们知道临界的那一层在 x 之下,只不过不知道是不是第 x 层,要用 x-1 层验证一下;要是没碎,问题就转化成 times ( k, n-x ),没坏的鸡蛋让楼下大爷帮忙收起来一会我接着用,临界的楼层就在 x 层的上面,在 x+1 到 n 层,这个时候我们就可以把第 x 层看成是地面,n 层还是 n 层,把这个楼房腰斩,只考虑上面那部分。然后这两个问题再依此向下递归,直到求出结果。

由于我们的 times ( k, n) 是最少的检测次数嘛,那在递归的时候肯定要的是两个递归方向中最小的那个,但是题解中给出的状态转移公式是这样的:

times(k,n)=1+min(1<=x<=n)(max(times(k-1,x-1),times(k,n-x)))

这里面我觉得比较难理解的,一个在为什么要+1,一个在为什么后面要有max?

我的理解是这样的:首先,加的那个 1 是因为,第一次鸡蛋,不管碎没碎,你都已经操作了一次,检测了一次,这个 1 一定要加上,然后后面的递归也是一样,每个子递归,都是以他为根节点的二叉树的第一个节点,都加上 1 。

其次,为什么要有 max ?我们不是要最小的吗?这里面涉及一个最坏情况的问题,比如我们有100层楼,但只有 2 个蛋,最好情况是一击必中,只有 1 次,坏的情况就是,比如他在99层是临界层,第一个蛋我们按照每10层一扔,在第100层碎了,那我们就知道,临界位置在91~100之间,然后我们用一个鸡蛋,一个一个尝试,试到99层,碎了,这时候我们检测了19次,如果在91层是临界,那我们只需要检测11次,19>11,19就是最坏情况下的最少次数。可能有的朋友会问,那我20层20层那么测试不行吗,按照每20层扔一次,我们能得到的是81~100有临界,然后再扔19次,这就是29次,肯定不是最好的。

现在理解了状态转移方程,我们就可以着手代码的编写。

先上代码:

class Solution {
    unordered_map<int, int> memo;
    int dp(int k, int n) {
        if (memo.find(n * 100 + k) == memo.end()) {
            int ans;
            if (n == 0) {
                ans = 0;
            } else if (k == 1) {
                ans = n;
            } else {
                int lo = 1, hi = n;
                while (lo + 1 < hi) {
                    int x = (lo + hi) / 2;
                    int t1 = dp(k - 1, x - 1);
                    int t2 = dp(k, n - x);

                    if (t1 < t2) {
                        lo = x;
                    } else if (t1 > t2) {
                        hi = x;
                    } else {
                        lo = hi = x;
                    }
                }

                ans = 1 + min(max(dp(k - 1, lo - 1), dp(k, n - lo)),
                                   max(dp(k - 1, hi - 1), dp(k, n - hi)));
            }

            memo[n * 100 + k] = ans;
        }

        return memo[n * 100 + k];
    }
public:
    int superEggDrop(int k, int n) {
        return dp(k, n);
    }
};

时间复杂度:O(knlogn)。我们需要计算 O(kn) 个状态,每个状态计算时需要 O(logn) 的时间进行二分查找。

空间复杂度:O(kn)。我们需要 O(kn) 的空间存储每个状态的解。

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode-solution-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

开幕雷击,这个unordered_map是什么东西?

以下部分内容参考自文章unordered_map 简介

无序映射(Unordered maps)是用于存储 键值key 和 映射值value 组合成的元素的关联容器,并允许基于其键快速检索各个元素。在unordered_map中,键值通常用于唯一地标识元素,而映射值是具有与该键关联的内容的对象。键的类型和映射的值可能会有所不同。

看到键值和映射值,大家有没有想到昨天的哈希表?如果想到了,那证明大家对哈希表的理解有了进步,因为我想到了,而且我觉得我进步了:)

unordered_map内部实现了一个哈希表。因此,其元素的排列顺序是无序的。

这道题里这个unordered_map是干什么用的呢?根据题解,这个算是数据字典的作用,是记录每个状态的值的,然后根据当时发生的情况,再有取舍的更新,这就是动态规划的思路。

那每个状态的值怎么求?这个时候就用到了二分查找了。我们把扔鸡蛋的第 x 层,赋值为中间那一层,然后 t1 和 t2 都是两边的最小次数,分着求就行,记得更新顶楼和地面。

然后memo.find ( n*100+k ) == memo.end()是unordered_map的查找方法,C++中的unordered_map常见用法详解中写的很清楚,大家可以去看看,我就不详细解释了,大概意思就是,我的哈希函数是n*100+k,然后我要在memo的哈希表里面查找对应的值,把那个值找出来,跟memo里最后一个元素比较,看看等不等,相等的话就开算,不等的话返回n*100+k的那个值。

这里我的问题是,为什么哈希函数是这个。。。还是我随便设置就可以?求大佬解答。。。

现在跟大家分享一下我看到的比较讨巧的方法:

class Solution {
public:
    int calcF(int K, int T)
    {
        if (T == 1 || K == 1) return T + 1;
        return calcF(K - 1, T - 1) + calcF(K, T - 1);
    }

    int superEggDrop(int K, int N)
    {
        int T = 1;
        while (calcF(K, T) < N + 1) T++;
        return T;
    }
};

作者:ikaruga
链接:https://leetcode.cn/problems/super-egg-drop/solution/887-by-ikaruga/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

代码很短,但越简练的代码,他信息量就越大。

这个数学方法采用了逆向思维,原题不是给定我楼层数,问我需要检测几次嘛,这回我自己规定我能检测的次数,看看我到底能确定多少层楼的临界就好了。

先假设我只有一次机会,那我一次机会只能确定两个楼层是不是临界,只给我一个鸡蛋也是同理,因为你有一次机会,你有一万个鸡蛋也白扯,只能从一楼开始扔,跟只有一个鸡蛋是一个道理。

现在再多给我一次机会,那多给我的那次机会就变成了我的第二次机会,这第二次机会就分两种情况了,第一种是蛋碎了,第二种是蛋没碎。因为我们也不知道鸡蛋碎没碎,所以这两种情况所能确定的楼层数我们都要加起来,一直加到能确定的层数比总数+1还要大,这个时候我们得到的机会的次数就是所求的最小检测次数了。

为什么要+1,因为我们不能站在房顶上扔鸡蛋。

四、总结

这个题确实很困难,但是和昨天那个“中等题”相比,也没有那么困难,因为至少他牵扯到的知识我都明白,都学过。只能说我的知识面不行,知道的东西太少,所以每天进行复盘,进行每日一题对我而言还是很有必要的,还有就是数学思维对于编码而言也确实很重要。

然后今天的新知识就是unordered_map了,这里我没有深入地去学,只是让自己知道这个数据结构是怎么一回事,然后了解了一下find,end这种内置函数是什么意思,以后遇到在慢慢学,我现在的打法是遇到什么学什么,暂时不深入的去了解,需要的时候再说。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 11.4题要求我们实现一个简单的FTP客户端程序,可以连接到FTP服务器,上传和下载文件。 实现这个程序需要使用Java的Socket编程,通过建立TCP连接与FTP服务器进行通信。我们需要实现FTP协议中的命令,如USER、PASS、LIST、RETR、STOR等,以及处理服务器返回的响应信息。 具体实现步骤如下: 1. 建立TCP连接,连接到FTP服务器。 2. 发送USER命令,输入用户名。 3. 发送PASS命令,输入密码。 4. 发送LIST命令,获取FTP服务器上的文件列表。 5. 发送RETR命令,下载指定文件。 6. 发送STOR命令,上传指定文件。 7. 处理服务器返回的响应信息,根据响应码判断操作是否成功。 8. 关闭TCP连接,结束程序。 需要注意的是,FTP协议中的命令和响应信息都是以文本形式传输的,需要进行编码和解码操作。另外,FTP服务器可能会有不同的实现,需要根据实际情况进行调整。 以上就是实现一个简单的FTP客户端程序的基本步骤。 ### 回答2: 11.4节主要讲解Java反射技术。Java反射是指在运行时动态地加载和使用类、创建对象、调用方法或访问属性的机制。Java程序在运行时需要加载并使用类和对象,而Java反射机制则为Java程序提供了更加灵活、方便且具有扩展性的策略。 在Java反射机制中,可以通过Class类获取指定类的信息。Class类是Java反射机制的核心类,它包含了Java程序运行时的基本信息,例如类的名称、继承关系、成员变量和方法等。通过Class类可以获取指定类的Constructor、 Field和Method等信息, Constructor用于创建新的对象实例, Field用于访问类中的成员变量, Method则用于调用类中的方法。 Java反射机制的应用非常广泛,主要应用于框架设计、ORM框架、单元测试框架等。其中,框架设计中的注解处理器是Java反射机制的重要应用。注解处理器可以通过Java反射机制获取注解信息并进行相关操作,从而实现对程序的控制和扩展。 总之,Java反射机制是Java程序设计的重要技术之一,熟练掌握Java反射机制可以极大地提升程序设计的灵活性和可扩展性,使得程序设计更加优雅和高效。 ### 回答3: 11.4节主要介绍了Java的多线程和线程池。在多线程方面,Java提供了Thread和Runnable两个类。Thread类代表一个线程,而Runnable接口定义了一个任务,可以将其提交给Thread进行执行。 Java的线程池是一组已经初始化的线程,它们可以被任何需要执行任务的线程使用。线程池中的线程可以被重用,而不是在每个任务执行完后都被销毁。这种线程重用可以提高执行任务的效率。 Java线程池的实现类是ThreadPoolExecutor。它接受一个任务队列和一些可重用线程,来提供执行多项任务的操作。 ThreadPoolExecutor类有几个参数:核心线程数、最大线程数、线程空闲时间和任务队列。核心线程数是线程池中的线程数,最大线程数是线程池中最多能存在的线程数,线程空闲时间是线程在不执行任务时保持活动状态的时间,任务队列存放着还未执行的任务。 ThreadPoolExecutor类还提供了一些方法,如submit()方法,用于提交任务到线程池中执行;execute()方法,用于执行任务;shutdown()方法,用于关闭线程池。 Java的多线程和线程池可以提高Java程序的效率和并发性,在处理大量任务时十分有用。但是,使用时需要注意线程安全和死锁等问题。在程序设计中,需要合理地使用线程池,以充分利用多线程带来的优势。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值