关于O(1)快速乘和关于其特判的原因
O(1)快速乘
在模超过 int 范围的大质数的意义下, 乘法运算可能会超过 long long 的范围. 在不能使用 __int128 的情况下, 我们可以使用 long long .
long long mul(long long a, long long b) {
long long rt = 0;
while (b) {
if (b) (rt += a) %= mod;
(a += a) %= mod, b >>= 1;
}
return rt;
}
然而这个让复杂度带上一个 long long 范围内的性质, 发明了一个复杂度
long long mul(long long a, long long b) {
long long rt = a * b - ((long long)((long double)a / mod * b + 0.5)) * mod;
return rt < 0 ? rt + mod : rt;
}
上述代码的原理在于
由于代码中 (long long)((long double)a / mod * b + 0.5)) 肯定在 long long 范围内, 并且乘上 mod 后与 a * b 的差不超过 mod, 故正确性是有保证的.
特判
注意到代码中有两个奇怪的地方:
(long long)((long double)a / mod * b + 0.5)是四舍五入的形式, 而不是向下取整的形式.- 返回值是
rt < 0 ? rt + mod : rt而不是rt.
可以想到返回值的特判的原因是 "五入" 导致减数比向下取整要大了 mod , 那么我们将代码改为如下 Code 1 的向下取整而不特判的形式是否是可以的呢?
// Code 1
long long mul(long long a, long long b) {
long long rt = a * b - ((long long)((long double)a / mod * b)) * mod;
return rt;
}
本人经过测试, 得到的答案是否定的, 在网上查找资料也未能得出结论, 网上的某些错误结论甚至使我在错误的道路越走越远.
所幸最后我还是找到了问题的结症所在, 我将向下取整的代码调整一下后的得到了如下正确的代码:
long long mul(long long a, long long b) {
long long rt = a * b - ((long long)((long double)a / mod * b)) * mod;
if (rt >= mod) rt -= mod;
if (rt < 0) rt += mod;
return rt;
}
这说明我们直接得出的答案与正确值之差的绝对值是不大于 mod 的, 并且是它的一个整数倍, 这令我感到鼓舞, 冷静分析之后, 发现了 Code 1 错误的原因——精度误差.
考虑当 (long double)a / mod * b 的真实值是一个十分接近但小于某个整数 1 , 故我们得到的答案与正确值之差的绝对值是 mod 或
至此, 特判的原因已经完全清楚了.
回过头来看最初的的写法, 我们意识到 + 0.5 的真正原因不是 '四舍五入', 而是使计算出来的值不会比原来大, 这样就只需要特判一种情况, 从而达到减小常数的目的.
总而言之, 挺妙的*<|:-).