1. 程式人生 > 實用技巧 >模數不超過 long long 範圍時的快速乘

模數不超過 long long 範圍時的快速乘

筆者的話:使用前請確保評測系統的long double嚴格為16B !

模數不在 int 範圍內的乘法在 OI 中運用廣泛,例如Millar-Rabin,Pollard-Rho等等。這樣的乘法,直接乘慘遭 long long 溢位, WA 聲一片。冒險運用 __int128 ,一朝事發,傾家蕩產。誤食 $O(\log P)$ 的龜速乘,人傻常數大, TLE 也無處申訴。這個時候, $O(1)$ 的快速乘就能派上用場。

快速乘運用真正的long double解決這樣的一個問題:給定 $x,y,P$ 滿足 $0\le x,y<P$ ,計算 $xy\bmod P$ 。這裡 $P< 2^{63}$ 。注意 $P$ 不一定是質數。

首先分析取模, $xy\bmod P=xy-\lfloor\frac{xy}{P}\rfloor P=(xy-\lfloor\frac{xy}{P}\rfloor P) \bmod 2^{64}$ 。所以,算出 $\lfloor\frac{xy}{P}\rfloor$ 後可以巧妙地藉助自然溢位計算左右兩個乘法和中間的減法,得到該式對 $2^{64}$ 取模的結果,也就得到了該式。

於是我們只剩下了一個難點就是計算 $\lfloor\frac{xy}{P}\rfloor$ 。這肯定不能先乘再除,只能先除再乘再取整。考慮搬出16B 的 long double ,先除再乘。

既然運用了 long double ,無疑這會有精度誤差。極端情況是 $\frac{x}{P}$ 的第一個有效位在小數點後第一位,那麼 long double 從第六十五位開始出錯,因此誤差範圍在 $(-2^{-64},2^{-64})$ ,乘 $y$ 之後就為 $(-\frac{1}{2},\frac{1}{2})$ 。運用常見技巧將其加上0.5L,誤差範圍變為 $(0,1)$ 。於是,取整時誤差為 $0,1$ 之一,乘 $-P$ 以後,最終誤差為 $0,-P$ 之一。也就是說,設答案為 $a$ ,最後的計算結果 $r$ 為 $a-P,a$ 之一。

由於 P 在 long long 內,所以當 $0\le r<P$ 時其為第二類,直接返回 $r$ ,否則為第一類,返回 $r-P$ ,就完成了快速乘計算。時間比較充裕的話直接返回 $(r+P)\bmod P$ 即可。

經實測,在筆者膝上型電腦上,開啟O2進行1e8次此資料範圍內的乘法運算,快速乘耗時 6.248s ,而龜速乘耗時 55.043s ,並且計算結果相同,足以見得快速乘的優越性。

附程式碼:

快速乘

ll mul(ll x,ll y,ll P)
{ll z=x*y-(ll)((long double)x/P*y+0.5L)*P;return z<P?z:z+P;}

龜速乘

typedef unsigned long long ll;
ll mul(ll x,ll y,ll P){ll z=0;for(;y;(x<<=1)>=P&&(x-=P),y>>=1)y&1&&((z+=x)>=P&&(z-=P));return z;}