筛法-OI-WIKI

筛法

OI-WIKI
该随笔由OI-WIKI而来,只不过添加了我对代码的注释和一些缺失的。方便以后查询。

埃氏筛

如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。

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

int ehrlich(int n) {
    vector<bool> visit(n + 1); // 默认所有数是质数
    for (int i = 2; i <= n; i++) {
        if (!visit[i]) { // false 代表是质数
        // 从 i*i开始是因为小于i的一定被其他数标记过了
            for (int j = i * i; j <= n; j += i) {
                visit[j] = true;
            }
        }
    }
    int cnt = 0;
    for (int i = 2; i <= n; i++)
        cnt += !visit[i];
    return cnt;
}

要找到直到n为止的所有素数,仅对不超过 \(\sqrt n\) 的素数进行筛选就足够了

奇数筛

因为除 2 以外的偶数都是合数,所以我们可以直接跳过它们,只用关心奇数就好。

// 奇数筛  改进埃氏筛,只能计数
int ehrlich2(int n) {
    if (n <= 1) {
        return 0;
    }
    vector<bool> visit(n + 1);
    int cnt = (n + 1) / 2; // 全体奇数,(n+1)是为了应对 n=3 的特殊情况
    for (int i = 3; i * i <= n; i += 2) {
        if (!visit[i]) {
            for (int j = i * i; j <= n; j += 2 * i) { // 每次加2i保证都是奇数
                if (!visit[j]) {
                    visit[j] = true;
                    cnt--;
                }
            }
        }
    }
    return cnt;
}

欧拉筛(线性筛)

埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。

如果能让每个合数都只被其最小质因数标记一次,那么时间复杂度就可以降到 \(O(n)\) 了。

// 欧拉筛,是每一个合数都只由它的最小质因数标记

int euler(int n) {
    vector<bool> visit(n + 1);
    vector<int> prime;
    for (int i = 2; i <= n; i++) {
        if (!visit[i]) {
            prime.push_back(i);
        }
        for (int j = 0; j < prime.size(); j++) {
            if (i * prime[j] > n) {
                break;
            }
            visit[i * prime[j]] = true;
            if (i % prime[j] ==0) { // 此后的数的最小质因数一定是 prime[j], 不能由 prime[j+1] 标记
                break;
            }
        }
    }
    return prime.size();
}
  • 如果 i % prime[j] ==0 那么代表着 i 的最小质因数也是 prime[j],如果该氏为 false,则代表 i 与 prime[j] 互质 后续的其他拓展都依托该性质

筛法求欧拉函数

注意到在线性筛中,每一个合数都是被最小的质因子筛掉。比如设 \(p_1\)\(n\) 的最小质因子, \(n'= \frac{n}{p_1}\) ,那么线性筛的过程中 \(n\) 通过 \(n' \times p_1\) 筛掉。

观察线性筛的过程,我们还需要处理两个部分,下面对 \(n' \mod p_1\) 分情况讨论。
如果 \(n' \mod p_1 == 0\) ,那么 \(n'\) 包含了 \(n\) 的所有质因子。

\[\varphi(n)=n \times \prod_{i=1}^{s}{\frac{p_i-1}{p_i}} \newline =p_1 \times n' \times \prod_{i=1}^{s}{\frac{p_i-1}{p_i}} = p_1 \times \varphi(n')\]

那如果 \(n' \mod p_1 \neq 0\) 呢,这时 \(n'\)\(p_1\) 是互质的,根据欧拉函数性质,我们有:

\[\varphi(n)=\varphi(p_1) \times \varphi(n')=(p_1-1) \times \varphi(n') \]

vector<int> pri;
bool not_prime[N];
int phi[N];

void pre(int n) {
  phi[1] = 1;
  // i 就是 上面的 n‘
  for (int i = 2; i <= n; i++) {
    if (!not_prime[i]) {
      pri.push_back(i);
      phi[i] = i - 1;  // 质数的 欧拉函数值
    }
    for (int pri_j : pri) {
      if (i * pri_j > n) break;
      not_prime[i * pri_j] = true;
      if (i % pri_j == 0) {
        phi[i * pri_j] = phi[i] * pri_j;
        break;
      }
      phi[i * pri_j] = phi[i] * phi[pri_j];
    }
  }
}

筛法求解质因数的个数

如果只求单个数的质因数个数的话,可以使用质因数分解的套路

int f(int n) {
    int ans = 0;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            ans++;
            while (n % i == 0) {
                n /= i;
            }
        }
    }
    if (n > 1) {
        ans++;
    }
    return ans;
}

使用筛法求解质因数的个数是,有如下几个要点,设 \(p_1\)n 的最小质因数 \(n'=\frac{n}{p_1}\),在代码中表现为 i

  1. \(n' \mod p_1 == 0\) 说明 \(n\) 的质因数个数和 \(n'\) 一样 factor_count[i * primes[j]] = factor_count[i];
  2. \(n' \mod p_1 == 0\) 说明 \(n\) 的质因数个数 为 \(n'\) 的个数再加上 \(p1\) factor_count[i * primes[j]] = factor_count[i] + 1;
vector<int> countPrimeFactors(int n) {
    vector<int> is_prime(n + 1, 1);
    vector<int> primes;
    vector<int> factor_count(n + 1, 0);  // 存储每个数的质因数个数
    
    is_prime[0] = is_prime[1] = 0;
    
    for (int i = 2; i <= n; ++i) {
        if (is_prime[i]) {
            primes.push_back(i);
            factor_count[i] = 1;  // 素数的质因数个数为1
        }
        
        for (int j = 0; j < primes.size() && i * primes[j] <= n; ++j) {
            is_prime[i * primes[j]] = 0;
            
            if (i % primes[j] == 0) {
                factor_count[i * primes[j]] = factor_count[i];  // 不增加质因数个数
                break;
            } 
            factor_count[i * primes[j]] = factor_count[i] + 1;  // 增加一个新的质因数
        }
    }
    
    return factor_count;
}

筛法求约数个数

约数定理:若 \(n=\prod_{i=1}^{m}{p_i^{c_i}}\) 那么 \(d_i=\prod_{i=1}^{m}{c_i+1}\), \(d_i\)i 的约数和,并且 \(d_i\) 为积性函数
证明:\(p_{i}^{c_i}\) 的约数有 \(p_i^0 + p_i^1+...+p_i^{c_i}\) ,根据乘法原理就是\(d_i=\prod_{i=1}^{m}{c_i+1}\)

vector<int> pri;       // 存储素数的数组
bool not_prime[N];     // 标记是否为合数,not_prime[i]=true表示i是合数
int d[N];              // d[i]表示i的约数个数
int num[N];            // num[i]表示i的最小质因数的指数

d[1] = 1;  // 1的约数只有1本身
for (int i = 2; i <= n; ++i) {
    if (!not_prime[i]) {  // 如果i是素数
        pri.push_back(i); // 加入素数表
        d[i] = 2;        // 素数的约数个数为2(1和它本身)
        num[i] = 1;      // 素数的最小质因数指数为1(i^1)
    }
    // 筛去合数
    for (int pri_j : pri) {
        if (i * pri_j > n) break;  // 超过范围则退出
        not_prime[i * pri_j] = true;  // 标记为合数
        
        if (i % pri_j == 0) {  // 关键部分:i包含pri_j这个质因数
            num[i * pri_j] = num[i] + 1;  // 最小质因数指数+1
            d[i * pri_j] = d[i] / num[i * pri_j] * (num[i * pri_j] + 1);
            break;  // 保证每个合数只被最小质因数筛去
        } else {  // i不包含pri_j这个质因数
            num[i * pri_j] = 1;  // 新的最小质因数,指数为1
            d[i * pri_j] = d[i] * 2;  // 约数个数乘以2(新增一个质因数)
        }
    }
}

代码的实现逻辑

  1. 当i是素数时

    • 约数个数d[i] = 2(1和它本身)

    • num[i] = 1(因为它自己是唯一质因数,指数为1)

  2. 当i × pri_j是合数时,分两种情况:

    • i包含pri_j(i % pri_j == 0)

      • 这意味着i × pri_j的最小质因数仍然是pri_j,只是指数增加了1

      • 更新 num[i×pri_j] = num[i] + 1

      • 约数个数更新公式:d[i×pri_j] = d[i] / (num[i]+1) × (num[i]+2)
        (相当于把原来的(c+1)因子替换为(c+2))

    • i不包含pri_j(i % pri_j != 0)

      • 这意味着pri_j是i×pri_j的最小质因数(因为pri_j比i的任何质因数都小)

      • 设置num[i×pri_j] = 1

      • 约数个数d[i×pri_j] = d[i] × 2
        (相当于在i的约数个数基础上乘以(1+1),因为新增了一个质因数pri_j^1 0次和1次)

筛法求约数和

对于一个正整数 \(n\),其约数和 \(\sigma(n)\) 定义为 \(n\) 的所有正约数之和,即:

\[\sigma(n) = \sum_{d|n} d \]

其中 \(d|n\) 表示 \(d\)\(n\) 的正约数。

基于质因数分解的计算公式

\(n\) 的质因数分解为:

\[n = \prod_{i=1}^{k} p_i^{a_i} = p_1^{a_1} \times p_2^{a_2} \times \cdots \times p_k^{a_k} \]

其中 \(p_i\) 是质数,\(a_i\) 是对应的指数,则约数和可以表示为:

\[\sigma(n) = \prod_{i=1}^{k} \left(1 + p_i + p_i^2 + \cdots + p_i^{a_i}\right) = \prod_{i=1}^{k} \frac{p_i^{a_i+1} - 1}{p_i - 1} \]

性质

  1. 积性函数:若 \(m\)\(n\) 互质(即 \(\gcd(m,n)=1\)),则:

    \[\sigma(mn) = \sigma(m) \times \sigma(n) \]

  2. 特殊情况

    • 对于质数 \(p\)

      \[\sigma(p) = 1 + p \]

    • 对于质数的幂 \(p^k\)

      \[\sigma(p^k) = 1 + p + p^2 + \cdots + p^k = \frac{p^{k+1} - 1}{p - 1} \]

  3. 完全数:若 \(\sigma(n) = 2n\),则 \(n\) 称为完全数(即等于其真约数之和的数)。

vector<int> pri;       // 存储素数的数组
bool not_prime[N];     // 标记是否为合数,not_prime[i]=true表示i是合数
int g[N];              // g[i]表示i的最小质因数的等比数列和(p^0 + p^1 + ... + p^k)
int f[N];              // f[i]表示i的约数和

void pre(int n) {
  g[1] = f[1] = 1; // 1的约数只有1本身,约数和为1
  for (int i = 2; i <= n; ++i) {
    if (!not_prime[i]) {  // 如果i是素数
        pri.push_back(i); // 加入素数表
        g[i] = i + 1;     // 素数的g值为1 + p
        f[i] = i + 1;     // 素数的约数和为1 + p
    }
    // 筛去合数
    for (int pri_j : pri) {
        if (i * pri_j > n) break;  // 超过范围则退出
        not_prime[i * pri_j] = true;  // 标记为合数
        
        if (i % pri_j == 0) {  // 关键部分:i包含pri_j这个质因数
            g[i * pri_j] = g[i] * pri_j + 1;  // 更新等比数列和
            f[i * pri_j] = f[i] / g[i] * g[i * pri_j];  // 更新约数和
            break;  // 保证每个合数只被最小质因数筛去
        } else {  // i不包含pri_j这个质因数
            f[i * pri_j] = f[i] * f[pri_j];  // 约数和相乘(积性函数性质)
            g[i * pri_j] = 1 + pri_j;  // 新的最小质因数的等比数列和
        }
    }
}
}
  • 更新等比数列和的代码数学原理为: 假设原数列和为 \(\frac{1-q^n}{1-q}\) 当p增加到 \(n-1\) 时,式子变为 $$\frac{1-q^{n+1}}{1-q}=\frac{q \times (1-q^n)}{1-q}+1$$

    \[\frac{q \times (1-q^n)}{1-q}=\frac{q-q^{n+1}}{1-q}=\frac{(1-q^{n+1})+(q-1)}{1-q}=\frac{1-q^{n+1}}{1-q}-1 \]

  • 更新约数和的原理为: \(\left(1 + p_i + p_i^2 + \cdots + p_i^{a_i}\right)\) 变为 \(\left(1 + p_i + p_i^2 + \cdots + p_i^{a_i+1}\right)\) ,因此先除前者,再乘后者
posted @ 2025-04-06 22:51  Elizahone  阅读(118)  评论(0)    收藏  举报