您的解决方案有两个问题,都与浮点运算的使用有关。第一个问题是 Python floats 仅携带大约 16 位有效十进制数字的精度,因此只要您的答案需要超过 16 位有效数字左右(因此点前超过 6 位,点后超过 10 位),您几乎没有希望获得正确的尾随数字。第二个问题更微妙,甚至影响n 的小值。那是因为double rounding,您将舍入到 11 位十进制数字然后删除最后一位数字的方法存在潜在错误。以n = 33 为例。 n 的立方根,小数点后 20 位左右,为:
3.20753432999582648755...
当它在该点之后四舍五入到 11 位时,您最终得到
3.20753433000
现在去掉最后一个数字会得到3.2075343300,这不是你想要的。问题是,小数点后 11 位可能最终会影响第 11 位数字左侧的数字。
那么你能做些什么来解决这个问题呢?好吧,您可以完全避免浮点并将其简化为纯整数问题。我们需要某个整数 n 的立方根到小数点后 10 位(将最后一位向下舍入)。这相当于将10**30 * n 的立方根计算为最接近的整数,再次向下舍入,然后将结果除以10**10。所以这里的基本任务是计算任何给定整数n 的立方根的底。我无法找到任何有关计算整数立方根的现有 Stack Overflow 答案(在 Python 中仍然更少),所以我认为值得详细展示如何做到这一点。
计算整数的立方根非常容易(借助一点点数学知识)。有多种可能的方法,但一种既有效又易于实现的方法是使用Newton-Raphson 方法的纯整数版本。在实数上,牛顿求解方程x**3 = n 的方法采用x 到n 的立方根的近似值,并迭代以返回改进的近似值。所需的迭代是:
x_next = (2*x + n/x**2)/3
在实际情况下,您会重复迭代,直到达到某个所需的容差。事实证明,在整数上,基本上相同的迭代工作,并且在正确的退出条件下,它会给我们完全正确的答案(不需要公差)。整数情况下的迭代为:
a_next = (2*a + n//a**2)//3
(注意使用地板除法运算符// 代替上面通常的真正除法运算符/。)在数学上,a_next 恰好是(2*a + n/a**2)/3 的地板。
以下是基于此迭代的一些代码:
def icbrt_v1(n, initial_guess=None):
"""
Given a positive integer n, find the floor of the cube root of n.
Args:
n : positive integer
initial_guess : positive integer, optional. If given, this is an
initial guess for the floor of the cube root. It must be greater
than or equal to floor(cube_root(n)).
Returns:
The floor of the cube root of n, as an integer.
"""
a = initial_guess if initial_guess is not None else n
while True:
d = n//a**2
if a <= d:
return a
a = (2*a + d)//3
还有一些例子使用:
>>> icbrt_v1(100)
4
>>> icbrt_v1(1000000000)
1000
>>> large_int = 31415926535897932384626433
>>> icbrt_v1(large_int**3)
31415926535897932384626433
>>> icbrt_v1(large_int**3-1)
31415926535897932384626432
icbrt_v1 中存在一些烦恼和效率低下的问题,我们将很快解决。但首先,简要解释一下上述代码的工作原理。请注意,我们从假设大于或等于立方根的底的初始猜测开始。我们将证明这个属性是一个循环不变量:每次我们到达 while 循环的顶部,a 至少是floor(cbrt(n))。此外,每次迭代产生的值a 严格小于旧值,因此我们的迭代保证最终收敛到floor(cbrt(n))。为了证明这些事实,请注意,当我们进入while 循环时,有两种可能性:
案例 1。a 严格大于 n 的立方根。然后a > n//a**2,代码继续下一次迭代。写a_next = (2*a + n//a**2)//3,我们就有了:
a_next >= floor(cbrt(n))。这是因为(2*a + n/a**2)/3 至少是n 的立方根,而AM-GM inequality 又适用于a、a 和a 和n/a**2:这三个量的几何平均值正好是n 的立方根,所以算术平均值必须至少是n 的立方根。所以我们的循环不变量被保留用于下一次迭代。
a_next < a:因为我们假设a 大于立方根n/a**2 < a,因此(2a + n/a**2) / 3 小于a ,因此floor((2a + n/a**2) / 3) < a。这保证了我们在每次迭代中都能在解决方案上取得进展。
案例 2。a 小于或等于 n 的立方根。然后是a <= floor(cbrt(n)),但是从上面建立的循环不变量我们也知道a >= floor(cbrt(n))。所以我们完成了:a 是我们所追求的值。而此时循环退出,因为a <= n // a**2。
上面的代码有几个问题。首先,从n 的初始猜测开始是低效的:代码将花费其最初的几次迭代(大致)将a 的当前值除以3,直到它进入解决方案的附近。初始猜测的更好选择(并且可以在 Python 中轻松计算)是使用超过 n 的立方根的 2 的第一次幂。
initial_guess = 1 << -(-n.bit_length() // 3)
如果n 足够小以避免溢出,更好的是使用浮点运算来提供初始猜测,例如:
initial_guess = int(round(n ** (1/3.)))
但这给我们带来了第二个问题:我们算法的正确性要求初始猜测不小于实际整数立方根,并且随着n 变大,我们不能保证对于基于浮点数的上面的initial_guess(尽管对于足够小的n,我们可以)。幸运的是,有一个非常简单的解决方法:对于 any 正整数 a,如果我们执行一次迭代,我们总是会得到一个至少为 floor(cbrt(a)) 的值(使用相同的 AM-GM 参数我们上面使用的)。因此,我们所要做的就是在开始测试收敛之前至少执行一次迭代。
考虑到这一点,下面是上述代码的更高效版本:
def icbrt(n):
"""
Given a positive integer n, find the floor of the cube root of n.
Args:
n : positive integer
Returns:
The floor of the cube root of n, as an integer.
"""
if n.bit_length() < 1024: # float(n) safe from overflow
a = int(round(n**(1/3.)))
a = (2*a + n//a**2)//3 # Ensure a >= floor(cbrt(n)).
else:
a = 1 << -(-n.bit_length()//3)
while True:
d = n//a**2
if a <= d:
return a
a = (2*a + d)//3
有了icbrt,可以轻松地将所有内容放在一起计算立方根,精确到小数点后十位。在这里,为了简单起见,我将结果输出为字符串,但您也可以轻松构造一个 Decimal 实例。
def cbrt_to_ten_places(n):
"""
Compute the cube root of `n`, truncated to ten decimal places.
Returns the answer as a string.
"""
a = icbrt(n * 10**30)
q, r = divmod(a, 10**10)
return "{}.{:010d}".format(q, r)
示例输出:
>>> cbrt_to_ten_places(2)
'1.2599210498'
>>> cbrt_to_ten_places(8)
'2.0000000000'
>>> cbrt_to_ten_places(31415926535897932384626433)
'315536756.9301821867'
>>> cbrt_to_ten_places(31415926535897932384626433**3)
'31415926535897932384626433.0000000000'