素数
素数的定义
只有1和其本身两个约数的数是素数,如2,5,11等
除了1和其本身还有其他的约数的数是合数,如4,8,20等
1和0既不是素数也不是合数。
素数非常的稀疏,n以内的素数大约只有$frac{n}{ln n}$个
素数的判定
试除法
枚举2~n-1,看看能不能整除n。
这是最暴力的方法,时间复杂度$O(n)$
对上述算法优化,约数是成对出现的所以枚举到$sqrt{n}$即可,时间复杂度$O(sqrt{n})$
威尔逊定理
p是素数的充要条件是$(p-1)! equiv -1 mod p$
证明思路来自《初等数论——命题人讲座》
证明:
p=1时显然不行。
先证充分性
这里我们要引入数论倒数的概念。
设$gcd(a,m)=1$,则$ax equiv 1 mod m$的解为$a$的数论倒数,记为$a^{-1}$。显然$aa^{-1} equiv 1 mod m$。
我们需要先证明两个引理。
引理1 对于$a in [2,p-2]$,$a^{-1} in [2,p-2]$ 且 $a eq a^{-1}$。
证明:
若$a^{-1} in {1, p-1}$,则$aa^{-1} in {a,p-a}$,显然不是$1$,所以$a^{-1} in [2,p-2]$。
若$a=a^{-1}$,则$aa^{-1}=a^2 equiv 1 mod p$,即$(a-1)(a+1) equiv 0 mod p$,因为$a in [2,p-2]$,所以这是不可能的(两个与p互质的数乘起来一定与p互质)。
引理1得证.
引理2 对于$2 leq a < b leq p-2$有$a^{-1} eq b^{-1}$
证明:
若$a^{-1} = b^{-1}$则$a^{-1} equiv b^{-1} mod p$,则$a equiv b mod p$,矛盾。
引理2得证.
现在我们只需将$a in [2,p-2]$与$a^{-1}$配对即可。
充分性得证。
必要性显然(若p为合数则取一个质因子即可),这里就不证了。
所以威尔逊定理得证。
这样可以先$O(n)$预处理然后$O(1)$判断。
Miller-Rabin素性测试
上述算法对于大数来说还是太慢了。
接下来介绍一下一种更快的算法——Miiler-Rabin素性测试
费马小定理:如果p为质数,则对于$a in [1,p-1]$,有$a^{p-1} equiv 1 mod p$。
我们反过来用$a$来判断$p$,可以做到$log$的复杂度。
但很遗憾这样做是错的。
我们称能通过费马小定理但不是素数的数称为费马伪素数或卡迈尔数。
毒瘤出题人可以通过特殊构造数据卡掉你的算法。
二次探测定理:如果p为质数,则$x^2 equiv 1 mod p$且$x in [1, p-1]$的解为$x=1,p-1$。
我们执行费马测试加入二次探测。
具体来讲就是把p-1分解成$2^t times s$,然后在平方时加入二次探测定理,最后再判断费马小定理。
已经证明若随机选择k个底数则这样做出现伪素数的概率为$frac{1}{4^k}$
代码:
//计算 (a^b)%m int ksm(int a, int b, int m) { int ret = 1; while (b) { if (b & 1) ret = (ret * a) % m; a = (a * a) % m; b >>= 1; } return ret; } bool Miller_Rabin(int n) { if (n < 3) return n == 2; int s = n - 1, t = 0; while (!(s & 1)) t++, s >>= 1; for (int i = 1; i <= 10; i++) {//随机选取10个底数 int now = rand() % (n - 2) + 2; now = ksm(now, s, n); for (int j = 0; j < t; j++) { int nxt = (now * now) % n; if (nxt == 1 && now != 1 && now != n - 1) return false;//二次探测 now = nxt; } if (now != 1) return false;//费马小 } return true; }
通常直接乘法会爆long long,所有要用龟速乘或long double。
对于小于$10^16$的数用$2,3,5,7,11,13,17,61,24251$作为底数即可。
素数筛选
质数筛选其实就是叫你把1-N的质数全部搞出来
直接判断
枚举每个数,看看它是不是质数,时间复杂度$O(nsqrt{n})$
for (int i = 2; i <= n; i++) { bool flag = 1; for (int j = 2; j * j <= i; j++) { if (i % j == 0) flag = 0; } if (flag) { prime[++m] = i; } }
埃式筛法
枚举一个质数,把它的倍数筛掉。
时间复杂度$O(n+frac{n}{2}+frac{n}{3}+cdots+frac{n}{n})=O(nlog{n})$
bool mark[N]; for (int i = 2; i <= n; i++) { if (!mark[i]) { prime[++m] = i; for (int j = i + i; j <= n; j += i) { mark[j] = 1; } } }
我们发现这样一个数会被筛多次(比如6会被2和3筛),所以我们筛的时候可以从$i imes i$开始,因为对于$j < i$,$i imes j$一定会被小于$i$的质数筛。
这样做的时间复杂度为$O(n log{log{n}})$
线性筛
我们发现上述筛法还是有可能被多个质因子筛(比如12还是会被2和3筛),所以我们可以考虑让每个数都只被其最小质因子筛一遍。
设当前枚举到的数是i,我们考虑用已经筛出来的素数prime[j]和i的乘积来筛剩下的数i*prime[j],为了让其只被其最小质因子筛一遍,所以如果i%prime[j]=0直接break就好。
bool mark[N]; for (int i = 2; i <= n; i++) { if (!mark[i]) { prime[++m] = i; } for (int j = 1; j <= m && i * prime[j] <= n; j++) { mark[i * prime[j]] = 1; if (i % prime[j] == 0) break; } }
质因数分解
唯一分解定理
所有的合数都可以分解成下面的形式:
$p_1^{c_1} imes p_2^{c_2} imes cdots imes p_m^{c_m}$,一般$p_1 < p_2 < cdots <p_m$
试除法
枚举所有质数,然后判断是不是其因数,如果是再一直除下去求有多少个这个质因数。
注意枚举的因数只用到根号n,因为不可能有两个大于根号n的素因子。除到最后如果n不是1就说明n存在一个大于根号n的质因子,加到n的质因子序列中即可。
所以筛质数一般也只用筛到根号n。
for (int i = 1; prime[i] * prime[i] <= n && i <= m; i++) { if (n % prime[i] == 0) { p[++cnt] = prime[i]; while (n % prime[i] == 0) { n /= prime[i]; c[cnt]++; } } } if (n > 1) { p[++cnt] = n; c[cnt] = 1; }
Pollard-Rho
一个随机化算法,比较迷。
具体就是找到n的一个约数d,然后分治去求d的质因数分解和n/d的质因数分解。再用MR判断一下当前n是不是质数,如果是直接加到质因数序列即可。
至于怎么找到这个d,这就是随机化的思想了。
先搞出来一个x,y在[1,n-1]的范围内随机找,然后每次令x=f(x),y=f(f(y)),直到1<gcd(|x-y|,n)<n就退出循环gcd(|x-y|,n)就是n的一个约数。
f(x)=(x+d)%n, d=random[1,n-1]。
这样做的时间复杂度大约是O(n^{frac{1}{4}})
总结
质数大概就这些内容:
1,判断
2,素数筛
3,分解质因数
都不是很难,但是学其他数论算法的基础(比如线性筛)。
题目也不会出裸题,所以要练的话就多刷题见套路吧。
我觉得最容易考的套路就是质数一般只用打到根号吧。。。
习题与讲解
给定区间[L,R](L≤R≤2147483647,R-L≤1000000)
,请计算区间中素数的个数。
注意到L,R很大,而R-L很小所以我们可以考虑把所有[L,R]内的素数筛出来。
注意我们并不需要预处理出所有R以内的素数而只需预处理到$sqrt{R}$。
因为任意一个大于$sqrt{R}$的合数不会有两个大于$sqrt{R}$的素因子。
用这些素数把$[L,R]$内的数都筛一遍即可,类似埃式筛法,时间复杂度是$O(nlog{n})$级别的。
#include <iostream> #include <cstdio> #include <cmath> using namespace std; bool mark[1000010], mark2[1000010]; int p[1000010], tot; int l, r; void init(int n) { for (int i = 2; i <= n; i++) { if (!mark[i]) p[++tot] = i; for (int j = 1; j <= tot; j++) { if (p[j] * i > n) break; mark[p[j] * i] = 1; if (i % p[j] == 0) break; } } } int main() { cin >> l >> r; init(sqrt(r)); for (int i = 1; i <= tot; i++) { for (int now = r / p[i] * p[i]; now > p[i] && now >= l; now -= p[i]) { mark2[now - l] = 1; } } int cnt = 0; for (int i = 0; i <= r - l; i++) { if (!mark2[i]) cnt++; } cout << cnt; return 0; }