快速的方法来计算n! mod m其中m是素数?

我很好奇,如果有一个好办法做到这一点。 我目前的代码是这样的:

def factorialMod(n, modulus): ans=1 for i in range(1,n+1): ans = ans * i % modulus return ans % modulus 

但它似乎很慢!

我也不能计算n! 然后应用素数模,因为有时n很大,n! 明确计算是不可行的。

我也遇到http://en.wikipedia.org/wiki/Stirling%27s_approximation,并想知道这是否可以在这里以某种方式使用?

或者,我怎样才能创build一个recursion,memoized函数在C + +?

把我的评论扩展到一个答案:

是的,有更有效的方法来做到这一点。 但他们非常混乱。

所以除非你真的需要额外的performance,否则我不build议去实现这些。


关键是要注意模数(实质上是一个分数)将成为瓶颈操作。 幸运的是,有一些非常快速的algorithm可以让您在同一个数上多次执行模数。

  • 用不变整数除法乘法
  • 蒙哥马利减less

这些方法很快,因为它们实质上消除了模量。


单单这些方法应该会给你一个适度的加速。 为了真正有效率,您可能需要展开循环才能使IPC更好:

像这样的东西:

 ans0 = 1 ans1 = 1 for i in range(1,(n+1) / 2): ans0 = ans0 * (2*i + 0) % modulus ans1 = ans1 * (2*i + 1) % modulus return ans0 * ans1 % modulus 

但考虑到一些奇怪的迭代,并将其与上面链接的方法之一结合起来。

有些人可能会说,循环展开应该留给编译器。 我会反驳说,编译器目前还不够聪明,以展开这个特定的循环。 仔细看看,你会看到为什么。


请注意,虽然我的答案是语言不可知的,但主要是针对C或C ++。

n可以是任意大的

那么, n不能任意大 – 如果n >= m ,那么n! ≡ 0 (mod m) n! ≡ 0 (mod m) (因为m是factorial的定义之一)


假设n << m并且您需要一个确切的值,就我所知,您的algorithm无法得到更快的速度。 但是,如果n > m/2 ,则可以使用以下标识( Wilson's定理 – 谢谢@Daniel Fischer!)

(图片)

在大约mn限制乘法的次数

 (M-1)!  ≡-1(mod m)
 1 * 2 * 3 * ... *(n-1)* n *(n + 1)* ... *(m-2)*(m-1)
 N!  *(n + 1)* ... *(m-2)*(m-1)≡-1(mod m)
 N!  ≡ -  [(n + 1)* ... *(m-2)*(m-1)] -1 (mod m)

这给了我们一个简单的方法来计算n! (mod m) n! (mod m)乘以mn-1 ,加上一个模逆 :

 def factorialMod(n,模数):
     ANS = 1
    如果n <=模数// 2:
         #通常计算阶乘(范围()的右参数是唯一的)
        我在范围内(1,n + 1):
             ans =(ans * i)%模量   
    其他:
         #Fancypants方法大n
        我在范围内(n + 1,模数):
             ans =(ans * i)%模量
         ans = modinv(ans,modulus)
         ans = -1 * ans +模量
    返回ans%模量

我们可以用另一种方式来重新说明上述方程式,可能会或可能不会稍快一点。 使用以下标识:

(图片)

我们可以将方程改为

 N!  ≡ -  [(n + 1)* ... *(m-2)*(m-1)] -1 (mod m)
 N!  ≡ -  [(n + 1-m)* ... *(m-2-m)*(m-1-m)] -1 (mod m)
        (相反的条款顺序)
 N!  ≡ -  [( -  1)*(-2)* ... *  - (mn-2)*  - (mn-1)] -1 (mod m)
 N!  ≡ - ((1)*(2)* ... *(mn-2)*(mn-1)*(-1) (mn-1) ] -1 (mod m)
 N!  ≡[(mn-1)!] -1 *(-1) (mn) (mod m)

这可以用Python编写如下:

 def factorialMod(n,模数):
     ANS = 1
    如果n <=模数// 2:
         #通常计算阶乘(范围()的右参数是唯一的)
        我在范围内(1,n + 1):
             ans =(ans * i)%模量   
    其他:
         #Fancypants方法大n
        我在范围内(1,模数-n):
             ans =(ans * i)%模量
         ans = modinv(ans,modulus)

         #因为m是一个奇素数,(-1)^(mn)= -1,如果n是偶数,则+1如果n是奇数
        如果n%2 == 0:
             ans = -1 * ans +模量
    返回ans%模量

如果你不需要一个确切的值,生活变得更容易 – 你可以使用斯特林(Stirling)的近似来计算O(log n)时间的近似值(使用取平方的指数 )


最后,我应该提到,如果这是时间关键的,而且您正在使用Python,请尝试切换到C ++。 从个人的经验来看,你应该期望速度提高一个数量级甚至更多,仅仅是因为这正是本机编译代码所擅长的那种CPU限制的紧缩循环(不论何种原因,GMP似乎比Python的Bignum精细得多)

N! mod m可以用O(n 1/2 +ε )运算来计算,而不是原始的O(n)。 这需要使用FFT多项式乘法,并且仅对于非常大的n是值得的,例如n> 10 4

该algorithm的概要和一些时间可以在这里看到: http : //fredrikj.net/blog/2012/03/factorials-mod-n-and-wilsons-theorem/

如果我们想要计算M = a*(a+1) * ... * (b-1) * b (mod p) ,我们可以用下面的方法,假设我们可以加,减和乘mod p),得到运行时复杂度为O( sqrt(ba) * polylog(ba) )

为了简单起见,假定(b-a+1) = k^2是平方。 现在,我们可以把我们的产品分成k个部分,即M = [a*..*(a+k-1)] *...* [(b-k+1)*..*b] 。 这个产品中的每个因子都是p(x)=x*..*(x+k-1) ,对于适当的x

通过使用诸如Schönhage-Strassenalgorithm的多项式的快速乘法algorithm,以分而治之方式,可以p(x) in O( k * polylog(k) )find多项式p(x) in O( k * polylog(k) )的系数。 现在,显然有一个algorithmreplacek点在同一K度多项式在O( k * polylog(k) ) ,这意味着,我们可以计算p(a), p(a+k), ..., p(b-k+1)快。

这种将许多点代入一个多项式的algorithm在C.Pomerance和R.Crandall的书“Prime numbers”中描述。 最后,当你有这些k值时,你可以将它们乘以O(k)并得到所需的值。

请注意,我们所有的操作采取(mod p) 。 确切的运行时间是O(sqrt(ba) * log(ba)^2 * log(log(ba)))

扩展我的评论,这[100,100007]其中m =(117 | 1117):

 Function facmod(n As Integer, m As Integer) As Integer Dim f As Integer = 1 For i As Integer = 2 To n f = f * i If f > m Then f = f Mod m End If Next Return f End Function 

如果n =(m-1),那么可以通过http://en.wikipedia.org/wiki/Wilson的s_theorem n! mod m =(m-1)

也正如已经指出的那样! 如果n> m,mod m = 0

我在quora上find了这个函数:
用f(n,m)= n! mod m;

 function f(n,m:int64):int64; begin if n = 1 then f:= 1 else f:= ((n mod m)*(f(n-1,m) mod m)) mod m; end; 

可能打败使用一个耗时的循环,并乘以存储在string中的大数字。 而且,它适用于任何整数m。
我find这个函数的链接: https : //www.quora.com/How-do-you-calculate-n-mod-m-where-n-is-in-the-1000s-and-m-is-一个-超大型素数-例如-正-1000-M-10-9 + 7

该algorithm具有O(n log log(n))预处理时间复杂度(由于筛选)和O(r(n))空间复杂度,其中r(n)是主要计数函数 。 预处理后,查询x! 其中x <= NO(r(x) * log(x))

不幸的是,这对10 10是不可行的,因为r(1e9)是455,052,511,需要几乎2GB的内存来存储整数。 然而对于10 9 ,我们只需要大约300MB的内存。

假设mod是1e9 + 7来避免C ++的溢出,但它可以是任何东西。

一种计算N的方法! mod m快

计算N! 快,我们可以将N分解为它的主要因素。 可以观察到,如果p是N!的一个主要因素,那么p必须<= N,因为N! 是从1到N的整数的乘积。鉴于这些事实,我们可以计算N! 仅使用2到N(包含)之间的素数。 根据维基百科 ,有大约5,847,534(〜5 * 10 ^ 7)的素数小于或等于10 9 。 这将是使用的公式:

v p(N!)是使得p v p (N!)除以N!的最大数目。 Legendre的公式计算O(log(N))时间内的v p (N!)。 这里是维基百科的公式。

https://wikimedia.org/api/rest_v1/media/math/render/svg/70c1119f9b33535f8812a372cb7fee3237efc838

 int factorial(int n) { int ans = 1; for (int i = 0; i < primes.size() && primes[i] <= n; i++) { int e = 0; for (int j = n; j > 0; ) { j /= primes[i]; e += j; } ans = ((u64) ans * fast_power(primes[i], e)) % mod; } return ans; } 

fast_power(x, y)返回O(log(y))时间中的x y %mod,使用平方指数 。

生成素数

我正在使用Eratosthenes筛生成素数。 在我的sieve的实现中,我使用bitset来节省内存。 当我使用第二代i3处理器编译-O3标志时,我的执行花费了16-18秒来产生高达10 ^ 9的素数。 我相信有更好的algorithm/实现来产生素数。

 const int LIMIT = 1e9; bitset<LIMIT+1> sieve; vector<int> primes; void go_sieve() { primes.push_back(2); int i = 3; for (int i = 3; i * i <= LIMIT; i+=2) { if (!sieve.test(i)) { primes.push_back(i); for (int j = i * i; j <= LIMIT; j += i) { sieve.set(j); } } } while (i <= LIMIT) { if (!sieve.test(i)) primes.push_back(i); i+=2; } } 

内存消耗

根据维基百科 ,有50,847,534素数小于或等于10 ^ 9。 由于32位整数是4个字节,因此主要向量需要203.39 MB(50,847,534 * 4个字节)。 筛子位需要(125 MB)10 ^ 9位。

性能

我能计算出10亿! 在1.17秒后处理。

注意:我使用-O3标志启用了algorithm。

假设你select的平台的“mod”运算符足够快,那么你主要受限于你计算n!的速度n! 以及您可用于计算的空间。

那么这本质上是一个两步操作:

  1. 计算n! (有很多快速algorithm,所以我不会在这里重复)
  2. 以结果的mod

没有必要使事情复杂化,特别是如果速度是关键的组成部分。 一般来说,尽可能less的循环内的操作。

如果你需要计算n! mod m n! mod m ,那么你可能想要记忆从函数进行计算的值。 与往常一样,这是经典的空间/时间折衷,但查找表非常快。

最后,你可以结合记忆和recursion(如果需要,也可以使用蹦床),以使事情真的很快。