[LOJ143]质数判定(Miller-rabin素数测试+防相乘爆long long的技巧讲解)

|▌冷眼眸甩不掉的悲伤 提交于 2020-02-09 20:47:14

题面

https://loj.ac/problem/143

题解

Miller-rabin素数测试

是一种随机化算法,能够在较短的时间内判断出一个数是合数,还是很可能为素数。其出错的概率极小;当用于测试的p取遍前10个素数时,在\(3e18\)的范围内不会出错。

原理:1、费马小定理

p为素数的必要不充分条件是\(\forall (a,p)=1,a^{p-1}{\equiv}1{\mod p}\)。证明不再赘述,可以参见百度百科或《数学奥林匹克命题人讲座:初等数论》2.3节。

2、二次探测

只使用费马小定理时,出错概率仍然较大,尤其是对于Carmichael数n(在\(1e8\)以下有255个,最著名的、最小的Carmichael数是561),对于所有的与n互质的正整数a,都可以满足\(a^{n-1}{\equiv}1{\mod n}\)。因此,仅仅使用Fermat测试是不够的。

二次探测的原理是:如果\(x^2{\equiv}1{\mod p}\),p为素数,那么有\(x{\equiv}{\pm}1{\mod p}\)。证明显然,将1移项至左边,然后因式分解即可。

具体做法

设待测数为mod,若mod为偶数则可以直接判断。否则,我们将mod-1分解为\(2^t*n\),其中\(2{\nmid}n\)。然后,取与mod互质的数p,计算

\[p^n、 p^{2n}、 p^{2^2n} … p^{2^tn}{\% mod}\]

从第二个数开始,每一个数可以通过前一个数平方得到。

  • 如果最后一项不为1,那么根据原理1,p不是素数;
  • 如果第一项就为1,那么此次测试失败,需要更换p。但是,这样的概率极小。
  • 如果第一项不为1,且最后一项为1:我们寻找这个数列中的第一个1,其前一项如果不是n-1,那么根据原理2,p不是素数。

对于特定选取的p,经过一次test(p,mod)后,出错的概率在\({\frac{1}{4}}\)以下。证明见《算法导论》第31.8节。

在待测数\(mod{\leq}3e18\)的情况下,选取p为2,3,5,7,11,13,17,19,23这前9个素数,可以保证不出错。

对于一个待测数mod,Miller-rabin素数测试的时间复杂度为\(O(klogmod)\)。(其中k为测试的轮数)

防止相乘爆long long的小技巧

  • 此方法适用范围:x,y,mod均在\(1e18\)级别。
#define ll long long
#define ld long double

inline void Adjust(ll &x,ll mod){
    x = (x % mod + mod) % mod;
}

inline void Tms(ll &x,ll y,ll mod){
    x = x * y - (ll)((ld)x * y / mod) * mod;
    Adjust(x,mod);
}

在Miller-rabin素数测试中,由于数据范围很大,会碰到求xy%mod(其中x,y,mod在1e18级别)的问题,此时如果直接乘会爆long long。为了解决这个问题,我们可以利用自然溢出:若两个long long型变量\(x,y\),进行了某种运算\({\bigodot}\)(包括加、减、乘)后超出了\([-2^{63},2^{63})\)的范围,那么返回值是使得

\[x{\bigodot}y=2^{64}*t+r(-2^{63}{\leq}r<2^{63})\]

的r。在Tms()中,\(x*y\)\((ll)((ld)x * y / mod) * mod\)均属于这种情况。二者的相减也是。

我们设(假设没有溢出)\(x*y-(ll)((ld)x*y/mod)*mod\)的值为r',我们真实想要的数为r。由于long double的运算可能产生误差,所以实际上\(r'\)可能等于r或者\(r{\pm}mod\)。但是无论如何,\(r'\)\([-2^{61},2^{61}]\)内,与r关于mod同余的一个数。

而考虑到溢出,真实情况下,\(x*y-(ll)((ld)x*y/mod)*mod\)的返回值应该是与\(r'\)关于\(2^{64}\)同余的、在\([-2^{63},2^{63})\)内的一个数\(s\)。但是,我们发现\(r'\)不管是加上还是减去\(2^{64}\),都超出了\([-2^{63},2^{63})\),因此,一定有\(s=r'\)

然后为了消除可能有的误差,再进行一次Adjust操作即可。

代码

#include<bits/stdc++.h>

using namespace std;

#define ll long long 
#define ld long double
#define rg register

namespace ModCalc{
    inline void Adjust(ll &x,ll mod){
        x = (x % mod + mod) % mod;
    }
    
    inline ll Check(ll x,ll mod){
        Adjust(x,mod);return x;
    }
    
    inline void Tms(ll &x,ll y,ll mod){
        x = Check(x * y - (ll)((ld)x*y/mod) * mod,mod); 
    }
    
    inline ll Mul(ll x,ll y,ll mod){
        Tms(x,y,mod);return x;
    }
}
using namespace ModCalc;

ll pri[9] = {2,3,5,7,11,13,17,19,23};

inline ll power(ll a,ll n,ll mod){
    ll x = a,s = 1;
    while(n){
        if(n & 1)Tms(s,x,mod);
        Tms(x,x,mod);
        n >>= 1;
    }
    return s;
}

inline bool test(ll mod,ll p){
    if(mod == p)return 1; //mod=p需要特判,因为p的倍数一定不符合费马小定理
    ll n = mod - 1;
    while(!(n&1))n >>= 1;
    ll r = power(p,n,mod);
    if(r == 1)return 1; //运气不好,本轮测试失败
    while(n < mod - 1){
        ll x = Mul(r,r,mod);
        if(x == 1)return r == mod - 1;
        r = x,n <<= 1;
    }
    return 0;
} 

inline bool MR(ll mod){
    if(mod < 2)return 0;     
    if(!(mod&1))return mod == 2;
    for(rg ll i = 0;i < 9;i++)if(!test(mod,pri[i]))return 0;
    return 1;
}

int main(){
    ll mod;
    while(~scanf("%lld",&mod)){
        puts(MR(mod) ? "Y" : "N");
    } 
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!