64位整数乘法
前置知识:
- 在一些平台上,long long 可以无损转 long double(但是无论如何都不可以无损转 double)
- 对于无符号整数来说,如果\(x<y\),那么\(x-y\)会得到一个极大的数,相当于绕了一圈
然后可以得到正确代码如下:
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
ll mul(ull a, ull b, ull p)
{
a %= p;
b %= p;
ull c = (long double)a * b / p;
ull res = a * b - c * p;
return (ll)((res + p) % p);
}
int main()
{
ll a,b,p;
scanf("%lld%lld%lld",&a,&b,&p);
printf("%lld",mul(a,b,p));
return 0;
}
这是因为:
整个计算过程可以写成:
其中 RN 表示 IEEE 754 默认的“舍入到最近值,距离相同时取偶数”。
算法并不保证 \(c\) 一定是正确商,只保证它与正确商最多相差 1,然后再通过加减一次 \(p\) 修正。
1. a、b 和 p 的转换是否有误差
在常见的 x86 80 位扩展精度 long double 中,有 64 个二进制有效位。
而一个有符号 64 位整数的绝对值最多约为:
64 个二进制有效位足以精确表示所有 long long 整数。因此在这种平台上:
(long double)a
是精确转换,没有误差。
表达式:
(long double)a * b / p
执行时,b 和 p 也会先隐式转换为 long double,因此实际运算是:
(long double)a * (long double)b / (long double)p
所以转换阶段没有丢失整数位。
2. a*b 最多有 126 位,为什么只保留 64 位仍然可以
假设:
那么精确乘积 \(ab\) 最多需要约 126 个二进制位。
例如精确乘积规格化后可能是:
80 位扩展精度只能保留 64 个有效位:
后面的位确实会被丢弃,但不是简单截断。处理器会查看 guard、round、sticky 等尾部信息,把结果舍入到最近的可表示值。
设舍入后的乘积为:
对于 64 位有效精度,在“舍入到最近值”模式下:
也就是说,乘法虽然丢掉很多低位,但它的相对误差非常小。
3. 除以 \(p\) 后,乘法误差会变成多大
真正关心的是商:
因为 \(a,b<p\),所以:
从而:
舍入后的乘积除以 \(p\):
乘法舍入造成的商的绝对误差是:
因为:
所以:
这就是最重要的一步:
a*b虽然可能丢失很多低位,但这些误差除以 \(p\) 后,对最终商造成的误差小于 \(0.5\)。
这里并不需要恢复精确的 \(ab\),只需要商的误差足够小。
4. 除法本身还会再舍入一次
处理器还需要计算:
除法也会产生一次相对误差。设:
代入 \(\widehat m=ab(1+\delta_1)\):
也就是:
因此:
因为 \(x<p\le2^{63}-1\),所以:
因此最终浮点商与精确商满足:
这才是算法正确性的核心。
5. 最后转换成 long long 才是截断
代码:
long long c = (long double)a * b / p;
右侧先完成浮点乘除,得到 \(\widehat x\),然后转换为 long long。
因为这里是非负数,所以向零截断等价于下取整:
令精确商为:
其中:
由于:
所以截断后的 \(c\) 最多只可能是:
注意,这里并没有保证:
算法只保证最多偏差 1。
因此数学意义的 \(ab-cp\) 的范围在 \([-p,2p)\),所以最后对于 res 来说需要加上 p 之后再取模

浙公网安备 33010602011771号