红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【数学】质数

质数

定义

设整数\(p\ne0,\pm1\)。如果 p除了平凡约数外没有其他约数,那么称p 为 素数不可约数)。

若整数 \(a\ne0,\pm1\)。且 a不是素数,则称a为 合数

若整数的因数是素数,则该素数称为该整数的素因数

性质

  • 大于 1 的整数 a是合数,等价于存在整数 (d,e)((1<d,e<a))使得
    \[ a \;=\; d \times e. \]

  • 若素数 p有大于 1 的约数 (d),则 \(d \;=\; p.\)(即素数只有 1 和它自身为约数)

  • 任意大于 1 的整数 a都可以表示为素数的乘积:
    \[ a \;=\; p_1 p_2 \cdots p_k. \]

  • 对于合数 a,一定存在素数 \(p \le \sqrt{a}\)使得 \(p \mid a.\)

  • 素数有无穷多个。

  • 所有大于 3 的素数都可以表示为
    \[ 6n \pm 1,\quad n\in\mathbb{Z}. \]

判断质数

较小数据范围

从第一个质数2开始验证,一个个验证待测数n是否能被每个数整除,若任何一个可以整除n,则不是质数

注意,只需要验证到\(\sqrt{n}\)即可

因为若一个数x已经验证无法整除n,则对应的\(\frac{n}{x}\)之后的数无需验证,因为此时这之后的因子与x相乘的结果一定大于n,一直到\(\sqrt{n}\)\(\frac{n}{\sqrt{n}}=\sqrt{n}\),故该后面的都不需要验证

1
2
3
4
5
6
7
bool isPrime(long long n){
    if(n<=1) return false;
    for(long long i = 2; i*i<=n;i++){
        if(n%i==0) return false;
    }
    return true;
}

较大数据范围

对于较大的数字,可以使用Miller-Rabin测试

过程:

  • 每次选择1~n-1上的随机数字,或者指定一个比n小的质数进行测试
  • 经s次(一般20次以内)得到,s越出错概率越低,速度越慢

对于Java用户,其BigInteger类型内自带了一个判断质数的方法isProbablePrime(s),使用的测试方法就包含了Miller-Rabin测试,对于大于1e9的数据也不会出现溢出

对于CPP用户,需要使用__int128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef __int128 ll;
typedef pair<int,int> PII;
const int N = 2e5 + 10;
const int mod = 1e9 + 7;
ll n;
template<typename T> inline T read() {
    T x = 0, f = 1; char ch = 0;
    for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
    for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch - '0');
    return x * f;
}

template<typename T> inline void write(T x) {
    if(x < 0) putchar('-'), x = -x;
    if(x > 9) write(x / 10);
    putchar(x % 10 + '0');
}
ll qpow(ll a, ll n, ll mod){
  ll re = 1;
  while(n){
    if(n&1) re = (re*a)%mod;
    a = (a*a)%mod;
    n>>=1;
  }
  return re%mod;
}
vector<ll> p ={2,3,5,7,11,13,17,19,23,29,31,37};
bool miller_rabin(ll n){
  if(n<3||n%2==0) return n==2;
  ll u = n-1, t = 0;
  while(u%2==0) u/=2, t++;
  for(auto a:p){
    if(n==a) return 1;
    if(n%a==0) return 0;
    ll v = qpow(a, u, n);
    if (v==1) continue;
    ll s = 1;
    for(; s<=t;s++){
      if(v==n-1) break;
      v = (v*v)%n;
    }
    if(s>t) return 0;
  }
  return 1;
}
void solve() {
  n = read<ll>();
  if(miller_rabin(n)) puts("Yes");
  else puts("No");

  
}
int main()
{
  ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
  ll tomorin=read<ll>();
  while (tomorin--) solve();
  return 0;
}

质因数分解

对于数n,因数是成对分布的,故可以枚举每一个数i到\(\sqrt{n}\),若是合数说明i是他的一个质因子,让\(\frac{n}{i}\),直到无法再除,证明将当前质因子对应的因数除尽,再试下一个i

1
2
3
4
5
6
7
8
9
void f(int n){
    for (int i = 2; i*i<=n;i++){
        if(n%i==0){
            cout << i << endl;
            while(n%i==0) n/=i;
        }
    }
    if(n>1) cout << n << endl;
}

注意,若已经有一个素数表,时间复杂度将会从\(O(\sqrt{N})\)下降为\(O(\frac{\sqrt{N}}{\ln N})\)

质数筛

埃氏筛

用一个vis数组标记是质数还是合数,默认都是质数

从第一个质数2开始,计算\(2\times2\)\(2\times3\cdots\),这些数肯定是合数,将这些数对应的vis标记为合数,再遍历操作下一个质数

注意:每次标记只需要从质数\(i\times i\)开始往后每次加\(i\)即可,因为前面的肯定被小于该数的质数标记过了

另外,由于偶数除了2都是合数,我们可以选择跳过偶数来让内存和操作需求都减半

另另外,由于bitset的连续读写效率,我们可以将bool数组改为bitset来将布尔值压到一个bit中,可以显著减少内存消耗,这种方法称为位级压缩

时间复杂度:\(O(n\log(\log n))\)

1
2
3
4
5
6
7
8
9
10
vector<bool> prime(N, true);
void ehrlich(int n){
  for (int i = 2;i*i<=n;i++){
    if(prime[i]){
      for(int j = i*i;j<=n;j+=i){
        prime[j]=false;
      }
    }
  }
}

若只是计数,埃氏筛还可以改进:我们可以预估质数数量,若发现更多合数就减掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注意此时true表示合数
vector<bool> prime(N, false);
int ehrlich(int n){
  int cnt = (n+1)/2; // 默认偶数都是合数
    // 只需要判断奇数
  for (int i = 3;i*i<=n;i+=2){
    if(!prime[i]){
        // 只需要判断奇数
      for(int j = i*i;j<=n;j+=2*i){
          if(!prime[j]){
              prime[j]=true;
              cnt--;
          }
      }
    }
  }
}

欧拉筛

埃氏筛也有重复筛的情况,因此会慢一些

而欧拉筛保证了每个合数只被自己的最小质因子筛掉

时间复杂度\(O(n)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<bool> prime(N, true);
void eular(int n){
  int tprime[n/2+1];
  int cnt = 0;
  for (int i = 2;i<=n;i++){
    if(prime[i]){
      tprime[cnt++]=i;
    }
    for(int j = 0; j<cnt;j++){
      if(i*tprime[j]>n)break;
      prime[i*tprime[j]]=false;
      if(i%tprime[j]==0) break;
    }
  }
}

分段筛法

普通的线性筛在数据范围很大的时候会超时或耗尽内存,此时我们需要使用分段筛

思想:用普通筛筛出\(\sqrt{n}\)内的所有质数,然后将所需区间[l, r]分成一段或多段,并对段进行标记,用筛出的质数标记段中合数,并统计没被标记的质数即可

步骤:设右区间为r

  1. 计算\(\sqrt{r}\),用筛筛出这之内的数
  2. 定义布尔数组标记合数(初始都设为false,表示为可能质数),长度为\(r-l+1\)
  3. 对每个质数
    • 1ll*p*p>r则可以提前break,因为超出区间右端
    • 计算[l, r]中第一个需要标记的位置start = max(1LL*p*p, ((l + p - 1) / p) * 1LL * p),即从区间第一个p的倍数开始,p小的时候也可以避免把p本身标掉
    • 从start开始以p步长标记start-l位置为合数,数组标记为true
  4. 对于l==1时,需要特殊处理数组0下标为合数,因为1不是质数
  5. 最后统计数组中false的位置就是质数

注意:

  1. 要开longlong,避免乘法溢出
  2. 若r和r-l都很大,可以考虑分为多个块,对每个块都进行流程

code例:

洛谷P1835 素数密度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
vector<bool> prime(N, true);
ll tprime[N];
bool vis[N]={};
ll cnt = 0;
// 观察到数据规模2^31,如果单纯用线性筛会导致爆内存
// 这时需要分段筛法
void eular(ll n){
  
  for(ll i =2;i<=n;i++){
    if(prime[i]){
      tprime[++cnt] = i;
    }
    for(ll j = i+i;j<=cnt;j+=i){
      if(i*tprime[j]>n) break;
      prime[i*tprime[j]]=false;
      if(i%tprime[j]==0)break;
    }
  }
}
void solve() {
  ll l, r;
  cin >> l >> r;
  // 首先晒出√2^31内的质数,然后用这些质数作为因子来在l-r范围内筛出合数并标记(i*p为合数)
  // 遍历l-r,没有标记的就是质数
  eular(50000);
  if (l==1) vis[0]=1; // 特判l=1
  for(int i = 1;i<=cnt;i++){
    ll p = tprime[i];
    for(ll j = max(2ll, (l-1)/p+1)*p;j<=r;j+=p){ // 计算在区间 [l,r] 中,应该从哪个数开始把质数 p 的倍数标记为合数(分段筛的起点)
      if(j-l>=0) vis[j-l]=true; // 将l-r移动至0-(r-l)范围内,防止过大的下标爆数组
    }
  }
  ll ans = 0;
  for(ll i = 0;i<=r-l;i++)if(!vis[i]) ans++;
  cout<<ans;
}

筛法求约数个数

\(d\)表示\(i\)的约数个数,\(num_i\)表示\(i\)的最小质因子出现次数

约数个数定理

定理
\(n = \displaystyle\prod_{i=1}^{m} p_i^{c_i}\),则 \(d(n) \;=\;\prod_{i=1}^{m}(c_i + 1)\,.\)

证明
对每个质因子 \(p_i^{c_i}\),它的约数有 \(p_i^0,\;p_i^1,\;\dots,\;p_i^{c_i}\)\(c_i+1\) 个。由乘法原理可知, \(n=\prod_{i=1}^{m}p_i^{c_i}\)的约数总数就是 \(\prod_{i=1}^{m}(c_i+1)\,.\)

实现

由于\(d_i\)是积性函数,所以我们可以使用线性筛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
vector<int> pri;         // 存储筛出的素数列表
bool not_prime[N];       // 标记合数
int d[N];                // d[i] 表示 i 的约数个数
int num[N];              // num[i] 表示 i 分解后,最低位质因子 p 在 i 中的幂次

void pre(int n) {
  d[1] = 1;              // 1 的约数个数为 1
  for (int i = 2; i <= n; ++i) {
    if (!not_prime[i]) {
      // i 是素数
      pri.push_back(i);
      d[i] = 2;          // 素数 i 的约数只有 1 和 i 自身,共 2 个
      num[i] = 1;        // i = p^1, 此处最低质因子 p 的幂次为 1
    }
    // 用已知的素数列表,对 i 进行线性筛
    for (int p : pri) {
      if (i * p > n) break;
      not_prime[i * p] = true;   // 标记 i*p 为合数

      if (i % p == 0) {
        // 情况一:p 是 i 的最低质因子,i = … * p^k
        // 那么 i*p = … * p^(k+1),尾质因子 p 的幂次加 1
        num[i * p] = num[i] + 1;
        // 如果 i 的约数个数为 d[i],且 i 中 p 的幂次为 num[i],
        // 那么 i*p 中 p 的幂次就是 num[i]+1
        // 根据约数个数公式:若 n = p1^a1 * p2^a2 *…,d(n)=∏(ai+1)
        // 我们可以在已知 d[i] 的基础上做局部更新:
        //   d[i*p] = d[i] / (num[i] + 0) * (num[i] + 1)
        d[i * p] = d[i] / num[i] * (num[i] + 1);
        break;  // 线性筛保证每个合数只被最小质因子对应的 p 处理一次
      } else {
        // 情况二:p 不是 i 的最低质因子,在 i*p 中 p^1 首次出现
        num[i * p] = 1;
        // 新加了一个质因子 p,故约数个数 d[i*p] = d[i] * 2
        d[i * p] = d[i] * 2;
      }
    }
  }
}

筛法求约数和

\(f_i\) 表示 \(i\) 的约数和,\(g_i\) 表示 \(i\) 的最小质因子的 \(p^0 + p^1 + p^2 + \cdots + p^k \,.\)

用线性筛在 \(O(n)\) 时间内计算出 \(1\sim n\) 每个正整数的约数和 \(f[i]\),其中
- \(f[i]=\sum_{d\mid i}d\)
- \(g[i]\) 表示将 \(i\) 分解为最小质因子 \(p\) 的幂乘以其它部分后,单独对这段 \(p^k\) 求和: \[ g[i]=1 + p + p^2 + \cdots + p^k\,, \] 其中 \(p^k\parallel i\),且 \(p\)\(i\) 的最小质因子。

借助下面这个分块性质:
\[ \text{若 }i=p^k\times x\text{ 且 }\gcd(p,x)=1,\quad f(i)=\bigl(1+p+\cdots+p^k\bigr)\times f(x)=g[i]\times f(x)\,, \] 我们可以在线性筛中同时维护 \(g[i]\)\(f[i]\)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

vector<int> pri;        // 存储已筛出的素数列表
bool not_prime[N];      // 合数标记
int g[N], f[N];         // g[i]:i 中最小质因子 p 的 1+p+...+p^k 和
                        // f[i]:i 的约数和

void pre(int n) {
  // 初始化:1 的约数和为 1,g[1]=1
  g[1] = f[1] = 1;

  for (int i = 2; i <= n; ++i) {
    if (!not_prime[i]) {
      // i 是素数 p
      pri.push_back(i);
      // 对于素数 p:
      //   g[p] = 1 + p
      //   f[p] = 1 + p
      g[i] = i + 1;
      f[i] = i + 1;
    }
    // 用已筛出的素数列表对 i 进行线性筛
    for (int p : pri) {
      if (i * p > n) break;
      int ip = i * p;
      not_prime[ip] = true; // 标记合数

      if (i % p == 0) {
        // 情况一:p 是 i 的最小质因子,即 i = p^k * x
        // 那么 ip = p^(k+1) * x,x 与 p 互素
        // g[i] = 1 + p + ... + p^k
        // 则 g[ip] = 1 + p + ... + p^k + p^(k+1) = g[i]*p + 1
        g[ip] = g[i] * p + 1;
        // f[ip] = f(x) * g[ip],而 f[i] = f(x)*g[i]
        // 故 f[ip] = f[i] / g[i] * g[ip]
        f[ip] = f[i] / g[i] * g[ip];
        // 线性筛关键:找到 i 的最小质因子后立即 break
        break;
      } else {
        // 情况二:p 不是 i 的最小质因子,i 与 p 互素
        // ip 增加了一个新的质因子 p^1
        // g[ip] = 1 + p
        // f[ip] = f[i] * f[p] = f[i] * (1+p)
        g[ip] = 1 + p;
        f[ip] = f[i] * f[p];
      }
    }
  }
}