小技巧1——长整型:64位整数的乘法模运算

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。


以下讨论均基于C/C++

1. 问题引入

最近做了几道有关数学的题目,然后要用到这些较大整数的乘法(比如说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,p1018时,我们没有办法直接计算 a × b   m o d   p a\times b \bmod p a×bmodp的值—— a × b a\times b a×b会溢出。所以我们必须想些办法,不可能为了这只大一点点的东西写一个高精度。

对于这些大数的乘法,我先讲一下网络上的普遍做法。

2. 解决方法

2.0 暴力相加法

应该大家都知道我喜欢从 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.1 反复翻倍法

(正经讨论开始)

这个东西其实就是类似于快速幂(快速幂的一般写法通常称为反复平方法),这里只是将平方变为 × 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++*号)还是差了不少,因此人称龟速乘,和它的祖先快速幂形成了鲜明的对比

2.2 位运算技巧法

所以为了避免出现 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 0a,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×bpa×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 1819位,可以使用这种方法。

接下来我们的得到的是一个拖着好几位小数的浮点数,然后我们把它扔进一个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×bc×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)

3. 总结

技巧性很强,但要注意细节(而且小数据难以查错)。

你可能感兴趣的:(其他)