Barrett Reduction 算法学习笔记

在模数变化频率较小情况下的高效取模优化器。

取模运算,即计算 amodpa \bmod p,在各类数论、计数题中经常出现。但是计算机对取模运算的处理较慢,如果取模的次数很多,则会显著拖慢程序的速度。

考虑怎么加速这个运算,通常我们人工计算取模,使用的是下面这个式子:

amodp=aap×aa \bmod p = a - \left \lfloor \frac{a}{p} \right \rfloor \times a

还剩下一个 q=apq = \left \lfloor \dfrac{a}{p} \right \rfloor 没有逃掉除法,或许可以使用浮点数预先存下 pp 的倒数,但是浮点数的运算还是比较慢,不尽如人意。

考虑用位移运算去逼近 1p\dfrac{1}{p},弄出两个整数 m,km,k,使得 m2k1p\dfrac{m}{2^k} \approx \dfrac{1}{p}。那么 qq 就约等于 a×m2k\left \lfloor \dfrac{a \times m}{2^k} \right \rfloor,只要预先处理出 m2kpm \approx \dfrac{2^k}{p},之后的取模便拆成了速度快得多的乘法与位移,一劳永逸。

考虑如何定下 mm,即 kk 要怎么取。直观上看,mm 的取值范围不能太小,至少要达到最大值要可以达到 pp,于是可以解得 k2log2pk \ge 2 \lceil \log_2{p} \rceil。下面我们证明,当 k2log2pk \ge 2 \lceil \log_2{p} \rceil,我们取 m=2kpm = \left \lfloor \dfrac{2^k}{p} \right \rfloor,则得到的答案便是几乎正确的:

证明:

apq=aapm2k=a2k(2kpm)a - pq = a - \dfrac{apm}{2^k} = \dfrac{a}{2^k} (2^k - pm)

因为 m=2kpm = \left \lfloor \dfrac{2^k}{p} \right \rfloor,故 2kp1<m2kp\dfrac{2^k}{p} - 1 < m \le \dfrac{2^k}{p},可推出:

02kpm<p0 \le 2^k - pm < p

而通常而言 aa 是小于 pp 的两个数的和或积,不超过 p2p^2。而 k2log2pk \ge 2 \lceil \log_2{p} \rceil,则有 2kp22^k \ge p^2。综合一下便有 0a2k<10 \le \dfrac{a}{2^k} < 1

那就得出了:

0apq=a2k(2kpm)<p0 \le a - pq = \dfrac{a}{2^k} (2^k - pm) <p

即:

amodp=apqa \bmod p = a - pq

\Box

有的人可能对「几乎正确」这个词很迷惑,不是都得出了 0apq<p0\le a - pq < p 了吗,为什么不能把「几乎」去掉呢?

这是因为其实上面的 qq 都没有带上下取整号,所以实际上 apqa - pq 可能会更大。但是只要 kk 取得不要太离谱,具体的,kk 的误差在 2020 以内,基本上 apq=amodpa - pq = a \bmod p 取值只有 0,p0,p 两种情况,极少数有 2p2p 的情况,没有 3p3p 的情况,即只需要再多进行至多两次减法即可。

以上算法便是著名的 Barrett Reduction 算法,其实如果模数是静态常量并且开 O2 优化,编译器就已经加上了这个优化。

这个算法需要对每个模数预先执行一次除法操作来预处理 mm。那考虑十分极端的情况,每次取模的模数都不一样,那该算法的效率便比直接取模还要慢。但正常来讲模数个数与取模次数之间差了至少一个数量级,所以大多数情况下还是很好用的。

经测试,在 2632^{63} 即 long long 范围内大概效率是不加优化的 70%70\%;而在 210242^{1024} 范围内,由于高精度除法的效率实在是低,加了这个优化后效率更是飞起,达到不加优化的 6%6\% 左右。