[数解算法] 扔鸡蛋问题
扔鸡蛋问题是一道经典的算法题,LeetCode链接。
- 一般做法是动态规划,时间复杂度,对应K个蛋F层楼。
- 优化后可以达到。
该题目下有很多算法详解,包括官方解答。
但是今天我们不聊动态规划,从数学的角度解决这个问题。
初步分析
有个鸡蛋,层楼,求最小次数。
易知,,即一个鸡蛋只能从一楼开始逐层尝试。
然而,当的时候,问题就难以通过简单的心算来得到答案。但是容易观察到:
即,2层楼和3层楼都需要扔2次。这一现象也很直观,不考虑蛋碎的情况,2次二分查找最多可以遍历3项。
同时,我们发现函数不是严格单调的,而是分段阶梯式,这非常不利于我们求解。
所以,我们不妨转化为问题,即给定鸡蛋和次数,最多可以遍历的楼层数。如果该问题得解,我们很容易求解原问题。
最多楼层
现在我们不需要标记楼层号了,而只需要知道自己遍历了多少层。
若当前有蛋,次机会,则在一次丢蛋后
- 剩余机会必然为
- 蛋可能为或者,对应向上和向下继续遍历
- 同时,当前楼层已经被遍历
所以我们有了下式
又由于1个蛋的情况我们已经得解,即
把带入中,可得
这不就变成了等差数列求和,所以我们有
如果我们把也带入中,即可得的通项。
虽然我们可以求解,但这离我们的通解还有一些距离。
求解
虽然我们无法直接求解通项,但是易知
(我们把看做的多项式,而为常数)
- 为次多项式
- 过点
这么一来,我们就有了个方程求解次方程。
记该多项式次幂的系数为,即有:
现在,我们就得到了解,对于个蛋,次机会,最多遍历楼层数为:
该解法的时间复杂度为,即矩阵求逆。
但是考虑到,如果时,可以直接求解,故
答题
至此,我们已经得到了题解,接下来我们把它转化为代码提交给LeetCode。
以下代码可以直接提交给LeetCode通过。
import numpy as np
import numpy.linalg as nl
class Solution:
def superEggDrop(self, egg: int, floor: int) -> int:
if 2 ** egg > floor:
return int(np.ceil(np.log2(floor + 1)))
M = np.array([[i ** j for j in range(egg + 1)] for i in range(egg + 1)])
R = np.array([[2 ** i - 1] for i in range(egg + 1)])
A = np.matmul(nl.inv(M), R)
A[0] = -floor
roots = np.roots(np.flip(A.T[0]))
root = [e.real for e in roots if np.isreal(e) and e.real > 0][0].real
return int(np.ceil(np.round(root, 6)))
其中M, R, A
对应前式相同符号。
唯一需要注意的是,python求根有精度误差,所以在结果返回前进行了一次6位精度的round。
我的解答的运行时间为76ms,对比官方答案需要184ms,相差三倍。
后记
在写完本文后,我查看了LeetCode上别人的解法。受到这个答案的启发,才发现自己组合数学确实不好。
式其实有非常简洁的形式。
我们已知组合恒等式
这与式非常相似。所以我们需要从中构造出一个多余的1。
将式从向展开,即
我们将个式子相加,右边最后一个式子的第一项正好是我们需要的,而去掉这一项后,此列正好剩下,正好满足我们的形式,即
所以,我们得到了式的另一个形式
import numpy as np
from scipy.special import comb
class Solution:
def superEggDrop(self, egg: int, floor: int) -> int:
if 2 ** egg > floor:
return int(np.ceil(np.log2(floor + 1)))
start = int(np.floor(np.log2(floor)))
for n in range(start, floor + 1):
if sum([comb(n, e + 1) for e in range(0,egg)]) >= floor:
return int(n)
然而,这种解法并不会比上面的多项式解法快,因为计算一次值得复杂度就已经是,而我们难以估计精确值,需要在较大的区间内搜索。
在LeetCode中运行,耗时浮动在100ms~150ms。耗时比前一种解法多50%。