组合数取模Lucas定理及快速幂取模

系统 1759 0

  组合数 取模就是求 的值,根据 的取值范围不同,采取的方法也不一样。

下面,我们来看常见的两种取值情况(m、n在64位整数型范围内)

(1)   ,

     此时较简单,在O(n 2 )可承受的情况下组合数的计算可以直接用杨辉三角递推,边做加法边取模。

(2) ,   ,并且 是素数

   本文针对该取值范围较大又不太大的情况(2)进行讨论。

     这个问题可以使用 Lucas 定理,定理描述:

 其中

    

     这样将组合数的求解分解为小问题的乘积,下面考虑计算 C(ni, mi) %p .

 已知C(n, m) mod p = n!/(m!(n - m)!) mod p。当我们要求(a/b)mod p的值,且a很大,无法直接求得a/b的值时,我们可以转而使用乘法逆元k,将a乘上k再模p,即(a*k) mod p。 其结果与(a/b) mod p等价。

那么逆元是什么?

    
      定义:满足a*k≡1 (mod p)的k值就是a关于p的乘法
      
        逆元
      
      (当p是1时,对于任意a,k都为1)
    
  

除法取模,这里要用到m!(n-m)!的逆元。

根据 费马小定理

已知gcd(a, p) = 1,则 a p-1  ≡ 1 (mod p),  所以 a*a p-2  ≡ 1 (mod p)。

也就是 (m!(n-m)!)的逆元为 (m!(n-m)!) p-2 ;

下面附上Lucas定理的一种证明,见下图,参考冯志刚《 初等数论 》第37页。

组合数取模Lucas定理及快速幂取模

题意: ,其中 ,并且 是素数。

代码:

        
          #include<iostream>


          
            //
          
          
            #include<algorithm>
          
          
            using
          
          
            namespace
          
          
             std; typedef 
          
          
            long
          
          
            long
          
          
             ll; 
          
          
            int
          
           quick_power_mod(
          
            int
          
           a,
          
            int
          
           b,
          
            int
          
           m){
          
            //
          
          
            pow(a,b)%m
          
          
            int
          
           result = 
          
            1
          
          
            ; 
          
          
            int
          
          
            base
          
           =
          
             a; 
          
          
            while
          
          (b>
          
            0
          
          
            ){ 
          
          
            if
          
          (b & 
          
            1
          
          ==
          
            1
          
          
            ){ result 
          
          = (result*
          
            base
          
          ) %
          
             m; } 
          
          
            base
          
           = (
          
            base
          
          *
          
            base
          
          ) %
          
            m; b
          
          >>=
          
            1
          
          
            ; } 
          
          
            return
          
          
             result; } 
          
          
            //
          
          
            计算组合数取模
          
          

ll comp(ll a, ll b, 
          
            int
          
           p) {
          
            //
          
          
            composite num C(a,b)%p
          
          
            if
          
          (a < b)   
          
            return
          
          
            0
          
          
            ; 
          
          
            if
          
          (a == b)  
          
            return
          
          
            1
          
          
            ; 
          
          
            if
          
          (b > a - b)   b = a -
          
             b; 
          
          
            int
          
           ans = 
          
            1
          
          , ca = 
          
            1
          
          , cb = 
          
            1
          
          
            ; 
          
          
            for
          
          (ll i = 
          
            0
          
          ; i < b; ++
          
            i) { ca 
          
          = (ca * (a - i))%
          
            p; cb 
          
          = (cb * (b - i))%
          
            p; } ans 
          
          = (ca*quick_power_mod(cb, p - 
          
            2
          
          , p)) %
          
             p; 
          
          
            return
          
          
             ans; } ll lucas(ll n, ll m, ll p) { ll ans 
          
          = 
          
            1
          
          
            ; 
          
          
            while
          
          (n&&m&&
          
            ans) { ans 
          
          = (ans*comp(n%p, m%p, p)) % p;
          
            //
          
          
            also can be recusive
          
          

        n /=
          
             p; m 
          
          /=
          
             p; } 
          
          
            return
          
          
             ans; } 
          
          
            int
          
          
             main(){ ll m,n; 
          
          
            while
          
          (cin>>n>>
          
            m){ cout
          
          <<lucas(n,m,
          
            10007
          
          )<<
          
            endl; } 
          
          
            return
          
          
            0
          
          
            ; }
          
        
      
View Code

  上面的代码中用到了求幂取模操作来计算(m!(n-m)!) p-2 % p.下面解释幂取模算法:

反复平方法 求a b %m

通过研究指数b的二进制表示发现,对任意的整数b都可表示为:


  • n表示b的实际二进制位数
  • b i 表示该位是0或1

因此,a b 可表示为:

即用b的每一位表示a的每一项,而对任意相邻的两项存在平方关系,即:

因此我们构造下面的算法:

    • 把b转换为二进制表示,并从右至左扫描其每一位(从低到高)
    • 当扫描到第i位时,根据同余性质(2)计算a的第i项的模:

      base变量表示第i-1位时计算出的模,通过递归能很容易地确定所有位的模。
    • 如果第i位为1,即b i =1,则表示该位需要参与模运算,计算结果 result = (result*base) mod m;其中result为前i-1次的计算结果;若b i =0,则表示a的第i项为1,不必参与模运算
    
      int quick_power_mod(int a,int b,int m){

    int result = 1;

    int base = a;

    while(b>0){

         if(b & 1==1){

            result = (result*base) % m;

         }

         base = (base*base) %m;

         b >>=1;

    }

    return result;

}
    
  

其中运用了两个同余性质:

同余性质1:ab≡bc (mod m)

同余性质2:   a≡c (mod m) => a 2 ≡c 2 (mod m)

理解要点:

  • base记录了a的每项的模,无论b在该位是0还是1,该结果都记录,目的是给后续位为1的项使用,计算方式是前一结果的平方取模,这也是反复平方法的由来
  • result只记录了位为1的项的模结果,该计算方式使用了同余性质1
  • 通过地把a使用二进制表示,并结合同余性质1,2,巧妙地化解了大数取模的运算。对1024位这样的大数,也最多进行1024次循环便可计算模值,性能非常快。

该方法是许多西方数学家努力的结果,通常也称为Montgomery算法。

(以上部分内容由网络搜集整理而来,不当之处,烦请不吝赐教)

 

组合数取模Lucas定理及快速幂取模


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论