笔记 · 素性判断等(Miller-Rabin,Pollard-Rho)

· · 个人记录

Miller-Rabin

两个引理

引理 1:费马小定理

p\in\text{Prime}a\in\mathbb{Z},且 \gcd(a,p)=1,那么有

a^{p-1}\equiv 1\pmod p

证明:

对于两个整数 a\ne b\pmod p,和一个和 p 互素的整数 c,则有 ac\ne bc\pmod p.

考虑两组数:1,2,\dots,p-1a,2a,\dots,(p-1)a。根据上述结论,两者均为模 p 的完全剩余系。

因此:

1\cdot2\cdot3\cdots(p-1)\equiv a\cdot 2a\cdot 3a\cdots (p-1)a\pmod p (p-1)!\equiv (p-1)!\cdot a^{p-1}\mod p \because \gcd((p-1)!,p)=1 \therefore a^{p-1}\equiv 1\pmod p

引理 2:二次探测定理

如果 p\in\text{Prime},且 0<x<p,则方程 x^2\equiv 1\pmod p 的解为 x=1,p-1.

证明:

易知 x^2-1\equiv 0\pmod p

\therefore (x+1)(x-1)\equiv 0\pmod p \therefore p|(x+1)(x-1) \because p\in\text{Prime},0<x<p \therefore x=1,p-1

算法流程

费马小定理的缺陷是,有一些特殊的整数,虽然 a^{n-1}\equiv 1\pmod n,但 n 是合数。我们可以考虑通过随机化的方式,来规避这种数,这也是 Miller-Rabin 算法的核心思想。

注意事项

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
int prime[10]={2,3,5,7,11,13,17,19,23,29};
int qmul(int a,int b,int mod){
    int ans=0;
    while(b){
        if(b&1) ans=(ans+a)%mod;
        a=(a+a)%mod;
        b>>=1;
    }
    return ans;
}
int qpow(int a,int b,int mod){
    int ans=1;
    while(b){
        if(b&1) ans=ans*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return ans;
}
bool Miller_Rabin(int n){
    if(n==2) return 1;
    if(n<2||!(n&1)) return 0;
    //特判0,1,2和偶数
    int s=0,t=n-1;
    while(!(t&1)){
        s++;
        t>>=1;
    }
    for(int i=0;i<10&&prime[i]<n;i++){
        int a=prime[i];
        int x=qpow(a,t,n);
        for(int j=1;j<=s;j++){
            int y=qmul(x,x,n);
            if(y==1&&x!=1&&x!=n-1) return 0;//二次探测检验
            x=y;
        }
        if(x!=1) return 0;//此时x=a^{n-1},用费马小定理
    }
    return 1;
}
signed main(){
    int n;
    cin>>n;
    cout<<(Miller_Rabin(n)?"YES\n":"NO\n");
}

Pollard-Rho

算法概述

该算法能快速找到大整数的一个非1、非自身的因子

原理这篇文章都讲透了

首先,可以考虑生成 [1,n] 的随机数 x,计算 d=\gcd(x,n),若 d>1,那么就找到了,d 就是一个 n 的一个因子。

最坏情况下,n=p^2p 是一个素数。那么当 x=p,2p,3p,\cdots,(p-1)p 时有 d>1,一共有 p-1\approx \sqrt{n} 个这样的 x,所以最差期望时间复杂度是 \mathcal{O}(\sqrt{n}\log n) 的。(log是算gcd带来的)

生日悖论

对于一个 [1,N] 内整数的理想随机数生成器,生成序列中第一个重复的数字前期望有 \sqrt{\frac{\pi N}{2}} 个数。(生日悖论)

对于 n=p^2,在 [1,n-1] 间生成的随机数模 p,大致是 [0,p-1] 间的随机数。因此期望在生成大约 \sqrt{p}=n^{\frac{1}{4}} 个数后,可以出现两个在模 p 意义下相同的数。那么这两个数的差 d,就满足 d\equiv 0\pmod p,也就满足 \gcd(d,n)>1,这时就找到了。但问题在于,我们没法两两进行比较验证,否则时间复杂度就会退回,还是没有进步。

伪随机数生成器

接下来是 Pollard-Rho 算法的精髓——使用一种特别的伪随机数生成器来生成 [0,n-1] 内的伪随机数序列。

设序列第一个数是 xf(x)=(x^2+c)\mod n

x,f(x),f(f(x)),\dots 是生成的伪随机数序列。

观察这个伪随机数生成器,刚开始的时候是随机的,到后面就会进入一个环。因为下一个数是依赖于上一个数的,而可生成的数又是有限的,因此会有环。所以这个伪随机数生成器生成的数,可以看作是一条链,最终连向一个环。看起来就像字母 \rho

我们可以使用龟兔赛跑算法(Floyd判环法)。

注意到这个伪随机数生成器有一个性质。

对于 |i-j|\equiv 0\pmod p,有

如果 $i,j$ 是环上满足条件的两个数,它们之间的距离为 $d$(两者之间间隔的边数),那么它们的后继 $f(i),f(j)$ 的距离也为 $d$,也满足条件。同理,$f(f(i)),f(f(j))$ 也满足条件…… 也就是说,如果环上两个相距为 $d$ 的数满足条件,那么环上所有相距为 $d$ 的数都满足条件。而在Floyd判环的过程中,由于两个指针的移动速度不同,每次移动都是在检查一个新的距离 $d$,而这样就无需两两比较了。 这个算法的复杂度依赖于该伪随机数生成器的随机程度。如果它是足够随机的,那么期望时间复杂度就是 $\mathcal{O}(n^{\frac{1}{4}}\log n)$。 #### 分块优化 这个 $\log$ 还是有点讨厌。怎么去除呢?我们发现瓶颈在于计算 $\gcd$。 注意到若 $\gcd(d,n)>1$,那么一定有 $\gcd(kd\mod n,n)>1$,所以我们可以先把一些待选数乘起来,再统一和 $n$ 求公因数,这样就减少了求 $\gcd$ 的次数。 所以我们可以每隔 $1,2,4,8,\dots$ 步求一次 $\gcd$。但这样到了后面就会走越来越多步才能退出,如果早就找到答案却迟迟不退出,显然不优,因此可以**在倍增取距离的基础上,设定一个固定步数上限 $C$**。当然也可以简单地每隔 $C$ 步就计算一下。这样时间复杂度至此优化至 $\mathcal{O}(n^{\frac{1}{4}}+\frac{n^\frac{1}{4}\log n}{C})$。当 $C>\log n$ 时,时间复杂度为 $\mathcal{O}(n^{\frac{1}{4}})$。 #### 注意事项: - 为了方便,通常令 $x,c$ 任取。 - 当 $n=4$ 时,无论 $c$ 取多少,都得不到解,需要特判。质数也直接特判返回。 - 一些细节和优化见代码。 ### 例题 模版:[P4718](https://www.luogu.com.cn/problem/P4718) 题意:$q(1\le q\le 350)$ 次询问,每次给出一个正整数 $n(2\le n\le10^{18})$,如果它是质数输出 ```Prime```,否则输出它最大的质因数。 #### Solution: 基本上就是 Pollar-Rho 和 Miller-Rabin 板子。注意,本题中用龟速乘会超时,只能强转 ```__int128```。 还需注意本题中要求找最大质因数。我们可以递归地解决该问题。对于当前数 $n$,Pollard-Rho 算法每次会找到一个 $n$ 的一个非1、非自身因数 $d$。我们可以递归地去解决数为 $d$ 和 $\frac{n}{d}$ 的子问题。注意往下递归之前判一下 $d$ 或者 $\frac{n}{d}$ 是否已经是质数了,因为Pollard-Rho算法规避找到自己的方法是“多试几次”,如果本来就是质数的话,就会一直循环循环…… 容易证明,递归的底层一定会得到原数的质因数,因为如果当前数是合数就能继续向下递归。我们取最大值即可。 #### Code: ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const __int128 one=1; int prime[12]={2,3,5,7,11,13,17,19,23,29,31,37};//n拓展到10^18之后,k=10不太够了,这里取k=12 int qpow(int a,int b,int mod){ int ans=1ll; while(b){ if(b&1) ans=one*ans*a%mod; a=one*a*a%mod; b>>=1; } return ans; } bool Miller_Rabin(int n){ if(n==2) return 1; if(n<2||!(n&1)) return 0; int s=0,t=n-1; while(!(t&1)){ s++; t>>=1; } for(int i=0;i<12&&prime[i]<n;i++){ int a=prime[i]; int x=qpow(a,t,n); for(int j=1;j<=s;j++){ int y=one*x*x%n; if(y==1&&x!=1&&x!=n-1) return 0; x=y; } if(x!=1) return 0; } return 1; } const int C=128; static std::mt19937 aleph; int Pollard_Rho(int n){ if(n==4) return 2; if(Miller_Rabin(n)) return n; std::uniform_int_distribution<int> rnd(3,n-1); int t=rnd(aleph),r=t,c=rnd(aleph); auto f=[=](int x){return (one*x*x%n+c)%n;}; t=f(t),r=f(f(r)); for(int lim=1;t!=r;lim=min(lim<<1,C)){ int fac=1ll; for(int i=0;i<lim;i++){ fac=one*fac*abs(t-r)%n; if(!fac) break;//小优化:如果当前乘积是0,后面也没必要乘了,怎么乘还是0,直接退出。 t=f(t),r=f(f(r)); } int d=gcd(fac,n); if(d>1) return d; } return n; } int ans=0; void mx(int z){ if(z>ans) ans=z;//优化,比max快 } void dfs(int n){ int d=Pollard_Rho(n); while(d==n) d=Pollard_Rho(n);//Pollard-Rho会随机找到一个因数,如果刚好找到了n,那就再做几次 int d2=n/d; if(Miller_Rabin(d)) mx(d);//递归下去前判一下质数,不然死循环 else dfs(d); if(Miller_Rabin(d2)) mx(d2); else dfs(d2); } int calc(int x){ if(Miller_Rabin(x)) return x; ans=0; dfs(x); return ans; } void solve(){ int n; scanf("%lld",&n); int x=calc(n); if(x==n) printf("Prime\n"); else printf("%lld\n",x); } signed main(){ srand(time(0)); int T; cin>>T; while(T--){ solve(); } } ```