本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
以下讨论均基于C/C++
。
最近做了几道有关数学的题目,然后要用到这些较大整数的乘法(比如说NOI 2018 屠龙勇士
中 1 0 12 10^{12} 1012级别的 p i p_i pi相乘,还有直接上到 1 0 18 10^{18} 1018级别的快速幂),这些在刚写代码的时候不容易想到问题,发现溢出了之后才想起这些数太大了。
这类问题虽然最终答案需要取模,但是当 a , b , p ≤ 1 0 18 a, b, p\le 10^{18} a,b,p≤1018时,我们没有办法直接计算 a × b   m o d   p a\times b \bmod p a×bmodp的值—— a × b a\times b a×b会溢出。所以我们必须想些办法,不可能为了这只大一点点的东西写一个高精度。
对于这些大数的乘法,我先讲一下网络上的普遍做法。
应该大家都知道我喜欢从 1 1 1开始编号,所以你看到这个方法的序号是 2.0 2.0 2.0你就知道这个算法没有什么实际意义。
(算了我还是讲一下吧,就是对于 a × b   m o d   p a\times b \bmod p a×bmodp重复 b b b次,每次 + a +a +a,然后取模,由于一般 a a a是给到 1 0 18 10^{18} 1018以内,所以相加不会溢出,复杂度是 O ( b ) O(b) O(b)的,其中 b b b是 1 0 18 10^{18} 1018级别的)
(正经讨论开始)
这个东西其实就是类似于快速幂(快速幂的一般写法通常称为反复平方法),这里只是将平方变为 × 2 \times 2 ×2。相信理解快速幂的同学一定会明白这是什么意思吧!我直接贴个代码:
inline long long qmul(long long a,long long b,long long p){
long long res=0;
while(b){
if(b&1)
res=(res+a)%p;
a=(a<<1)%p;b>>=1;
}
return res;
}
//备注:这个代码是写博客时直接手打,如有错误希望在评论区帮我指出。
当然这个复杂度和快速幂的复杂度一样,是 O ( log 2 b ) O(\log_2 b) O(log2b)的(较暴力相加法快了非常多是不是啊)。但是对于通常认为是 O ( 1 ) O(1) O(1)的直接相乘(即C/C++
的*
号)还是差了不少,因此人称龟速乘,和它的祖先快速幂形成了鲜明的对比。
所以为了避免出现 n = 1 0 6 n=10^6 n=106的大数据而导致的 O ( n log 2 a i ) O(n\log_2 a_i) O(nlog2ai)的复杂度被卡,
我们还是希望找到一种理论复杂度 O ( 1 ) O(1) O(1)的方法。这是存在的。
Notes:以下讨论均默认 0 ≤ a , b < p 0\le a,b < p 0≤a,b<p。如果出现负数或是超过模数的情况,请自行预先处理。
首先,我们先明确模运算的定义:
a × b   m o d   p = a × b − ⌊ a × b p ⌋ × p a\times b\bmod p=a\times b - \left \lfloor \frac{a\times b}{p} \right \rfloor\times p a×bmodp=a×b−⌊pa×b⌋×p
所以,当 a , b < p a,b<p a,b<p时,一定满足 ⌊ a × b p ⌋ < p \left \lfloor \frac{a\times b}{p} \right \rfloor < p ⌊pa×b⌋<p。
然后我们可以用long double
来存储 a × b p \frac{a\times b}{p} pa×b的值(浮点数形式),根据浮点数的运算规则,当精度不够用的时候,舍弃末尾也就是小数点后的几位,因此使用long double
即可满足要求(现在绝大多数平台上long double
给了 12 12 12或 16 16 16个字节,因此有效位数是 18 − 19 18-19 18−19位,可以使用这种方法。
接下来我们的得到的是一个拖着好几位小数的浮点数,然后我们把它扔进一个long long
里(不妨记为 c c c,那么 c = ⌊ a × b p ⌋ c=\left \lfloor \frac{a\times b}{p} \right \rfloor c=⌊pa×b⌋),以达到下取整的目的。(严重警告:请不要直接使用long double
将原式算出来,否则可能产生严重误差或发生溢出。请只在这一步使用)。然后我们就可以把答案看成 a × b − c × p a\times b-c\times p a×b−c×p了。
慢着,这里long long
类型的 a a a和 b b b相乘不会溢出吗?
是的, a × b a\times b a×b会溢出, c × p c\times p c×p也会溢出,但是无论如何,他们之间的差值总是在 [ 0 , p ) [0,p) [0,p)内,所以我们只需要关心最后的十几位即可,那些溢出的只是舍去了高位,对低位没有影响,因此答案是正确的。
综上,我们只是根据模运算的定义,然后结合了long double
字节长但是不精确的特点,避开了 64 64 64位的限制算出答案。技巧性非常强,但值得一学。
下面给出代码:
inline long long qmul(long long a,long long b,long long p){
a=(a%p+p)%p;b=(b%p+p)%p;//顺带处理了负数和过大的情况
long long c=a*(long double)b/p;//这里的c是准确的下取整结果
long long ans=a*b-c*p;
if(ans<0)//如果不在[0,p)之间则调整一下
ans+=p;
else if(ans>=p)
ans-=p;
return ans;
}
时间复杂度 O ( 1 ) O(1) O(1)。
技巧性很强,但要注意细节(而且小数据难以查错)。