1. 问题
今天在做leetcode 不同路径 的时候发现了个问题
对于m=53 n=4class Solution:def uniquePaths(self, m: int, n: int) -> int:rlt = 1for i in range(0, m-1):rlt *= (m + n - 2 - i)for i in range(0, m-1):rlt /= (i + 1)return int(rlt)为什么这个结果是 26234class Solution:def uniquePaths(self, m: int, n: int) -> int:up = 1down = 1for i in range(0, m-1):up *= (m + n - 2 - i)for i in range(0, m-1):down *= (i + 1)return int(up/down)而这个结果是 26235
据GPT描述,两个代码逻辑上是相同的,计算的是组合数 C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n−2m−1:
C m + n − 2 m − 1 = ( m + n − 2 ) ! ( m − 1 ) ! ( n − 1 ) ! C_{m+n-2}^{m-1} = \frac{(m+n-2)!}{(m-1)!(n-1)!} Cm+n−2m−1=(m−1)!(n−1)!(m+n−2)!
然而,两个代码的计算方式有所不同:
-
第一个代码 直接使用浮点除法
rlt /= (i + 1)
,在 Python 中,浮点数的计算可能会引入精度误差,最终int(rlt)
向下取整,导致结果偏小。 -
第二个代码 分别计算
up
和down
,然后用int(up/down)
,由于 Python 的整数运算是精确的,这种方式避免了浮点误差,得到了正确的组合数 26235。
第一个代码因为浮点误差,导致 int(rlt)
取整后比正确值少 1,所以结果是 26234,而第二个代码由于整数计算的精确性,得到了正确答案 26235。
2. 思考
即使除法的数学结果是整数,如果中间计算涉及浮点数,可能会因为浮点精度误差导致最终取整时丢失 1。我们来深入分析原因:
① Python 的 /
生成浮点数
Python 里的 /
即使除得的是整数,结果仍然是 float
。例如:
print(10 / 2) # 5.0
print(type(10 / 2)) # <class 'float'>
在代码中,这意味着:
rlt = int(rlt / (i + 1))
即使 rlt / (i + 1)
的数学结果是整数,Python 仍然会先计算 浮点数,然后 int()
强制取整,可能引入误差。
② 浮点数存储精度问题
Python 的浮点数是基于 IEEE 754 标准的 双精度浮点数(double precision)。但 某些整数在浮点数表示中无法精确存储,例如:
x = 10000000000000003
y = 10000000000000002print(float(x)) # 10000000000000004.0 (存储时发生变化)
print(float(y)) # 10000000000000002.0 (存储正确)
当 rlt
变大时,比如 C_{m+n-2}^{m-1}
计算过程中,rlt
可能会进入 浮点存储的误差范围,导致浮点数计算不精确。
③ int()
取整丢失 1
浮点数不能精确表示某些大整数时,取 int()
可能会导致向下取整。假设 rlt / (i + 1)
的数学结果是 26235
,但由于浮点精度问题,Python 存储的可能是:
26234.999999999998
此时 int(26234.999999999998)
向下取整,结果变成 26234,比正确答案少了 1。
④ 解决方案
避免浮点数参与计算,直接使用 整数除法 //
,确保所有计算都在 整数空间 进行:
rlt //= (i + 1) # 这样不会有浮点精度问题
这样,每次除法都保证是 整数运算,不会因浮点精度误差导致 int()
取整出错。
3. 进一步理解
为什么x = 10000000000000003
y = 10000000000000002print(float(x)) # 10000000000000004.0 (存储时发生变化)
print(float(y)) # 10000000000000002.0 (存储正确)
这个现象是由于 IEEE 754 双精度浮点数(double precision floating point) 的 有限精度 导致的。让我们来深入分析为什么 10000000000000003
在转换成 float
时变成了 10000000000000004.0
,而 10000000000000002
仍然能正确存储。
① 浮点数的存储方式
Python 的 float
类型遵循 IEEE 754 双精度浮点数标准,使用 64 位 进行存储:
- 1 位 表示符号位(正/负)。
- 11 位 表示指数(决定浮点数的范围)。
- 52 位 表示 尾数(精确存储的部分)。
由于 52 位尾数最多只能精确存储 52 位二进制数,所以如果一个整数的二进制表示超过 52 位,就必须 进行四舍五入,这可能导致数值发生变化。
② 10000000000000003
和 10000000000000002
的二进制表示
我们先看看这两个数的 二进制表示(十进制 → 二进制):
10000000000000002
10000000000000002 = 10001110000110111100100110101100111011001000000000000000000010 (二进制)
- 这个数的尾数(低 52 位)可以被精确存储,所以转换成
float
时不会损失精度。
10000000000000003
10000000000000003 = 10001110000110111100100110101100111011001000000000000000000011 (二进制)
- 这个数的尾数超过了 52 位的存储限制,所以 IEEE 754 必须进行四舍五入。
- 由于 IEEE 754 采用 “最接近偶数舍入”(round to nearest, ties to even),它会 向上舍入 到
10000000000000004
,因此float(10000000000000003)
变成了10000000000000004.0
。
③ 为什么 10000000000000003
变成 10000000000000004.0
?
当一个整数大到超出 52 位精度范围 时:
- 无法精确存储,需要进行近似表示。
- IEEE 754 采用“四舍五入到最近的偶数”规则:
10000000000000003
比10000000000000004
更接近10000000000000004
,所以它会被 向上舍入。- 如果它的二进制表示是
...00011
(奇数结尾),IEEE 754 会向上舍入到...00100
(偶数)。
所以:
print(float(10000000000000003)) # 10000000000000004.0
print(float(10000000000000002)) # 10000000000000002.0
这个现象就是 浮点数的精度限制 造成的,它在转换时 自动进行了四舍五入,使得 10000000000000003
变成了 10000000000000004.0
。
总结
- Python
float
遵循 IEEE 754 双精度浮点数(64 位),其中只有 52 位 用于存储尾数,超过部分会 四舍五入。 10000000000000002
可以精确存储,因为它的二进制表示没有超出 52 位精度范围。10000000000000003
由于超出 52 位精度范围,被四舍五入成10000000000000004.0
。
写在最后
本文采用了 ChatGPT 辅助进行内容的书写和完善