每日算法 — 使用java实现线性筛与积性函数:欧拉函数和莫比乌斯函数的线性预处理

算法概述

线性筛(Linear Sieve),又称欧拉筛,是一种在 $O(n)$ 时间内筛出所有素数并同时预处理积性函数的算法。与经典的埃拉托斯特尼筛($O(n \log \log n)$)相比,线性筛不仅复杂度更优,还能在筛的过程中顺便计算出所有数的最小质因子、欧拉函数 $\varphi(n)$、莫比乌斯函数 $\mu(n)$ 等重要的数论函数。它是算法竞赛中处理数论问题的核心基础工具。

核心思想

为什么线性筛是线性的?

传统筛法对每个合数可能被多次标记(如12被2和3各标记一次),而线性筛的核心保证是:每个合数只被它的最小质因子筛掉一次。

具体策略:
1. 维护一个质数列表 primes
2. 对于每个数 $i$,依次用 primes 中的质数 $p$ 去筛
3. 如果 $p$ 是 $i$ 的最小质因子(即 $i \mod p == 0$),则停止

这样,对于任意合数 $n = p \times k$($p$ 是 $n$ 的最小质因子),它只会在 $i = k$、$p$ 为当前质数时被筛一次。

积性函数的同步计算

积性函数满足:若 $\gcd(a, b) = 1$,则 $f(ab) = f(a)f(b)$。

线性筛过程中可以同步维护积性函数:
– 欧拉函数 $\varphi(n)$:小于等于 $n$ 且与 $n$ 互质的正整数的个数
– 莫比乌斯函数 $\mu(n)$:用于容斥和莫比乌斯反演
– 约数个数 $d(n)$、约数和 $\sigma(n)$ 等

Java 完整实现

import java.util.*;

/**
 * 线性筛(欧拉筛)完整实现
 * 同步预处理:素数表、最小质因子、欧拉函数、莫比乌斯函数、约数个数、约数和
 */
public class LinearSieve {

    static final int MAXN = 10000005;

    // 核心数组
    static boolean[] isComposite;  // 是否为合数
    static int[] primes;           // 素数列表
    static int primeCount;         // 素数个数
    static int[] spf;              // 最小质因子 (smallest prime factor)

    // 积性函数
    static int[] phi;              // 欧拉函数
    static int[] mu;               // 莫比乌斯函数
    static int[] d;                // 约数个数
    static long[] sigma;           // 约数和
    static int[] powP;             // 最小质因子的最高次幂
    static int[] cntP;             // 最小质因子的指数

    /**
     * 线性筛核心算法
     * 在O(n)时间内筛出素数,并同步计算所有积性函数
     */
    static void sieve(int n) {
        isComposite = new boolean[n + 1];
        primes = new int[n + 1];
        spf = new int[n + 1];
        phi = new int[n + 1];
        mu = new int[n + 1];
        d = new int[n + 1];
        sigma = new long[n + 1];
        powP = new int[n + 1];
        cntP = new int[n + 1];

        primeCount = 0;
        phi[1] = 1;
        mu[1] = 1;
        d[1] = 1;
        sigma[1] = 1;

        for (int i = 2; i <= n; i++) {
            if (!isComposite[i]) {
                // i是素数
                primes[primeCount++] = i;
                spf[i] = i;

                // 素数的积性函数值
                phi[i] = i - 1;        // φ(p) = p - 1
                mu[i] = -1;            // μ(p) = -1
                d[i] = 2;              // d(p) = 2 (1和p)
                sigma[i] = 1L + i;     // σ(p) = 1 + p
                powP[i] = i;           // p^1
                cntP[i] = 1;           // 指数为1
            }

            // 用已知的素数去筛
            for (int j = 0; j < primeCount && i * primes[j] <= n; j++) {
                int p = primes[j];
                int ip = i * p;
                isComposite[ip] = true;
                spf[ip] = p;

                if (i % p == 0) {
                    // p是i的最小质因子,即 i = p^k * m, gcd(p, m) = 1
                    // 此时ip = p^(k+1) * m

                    // 欧拉函数:φ(ip) = φ(i) * p (因为p | i)
                    phi[ip] = phi[i] * p;

                    // 莫比乌斯函数:μ(ip) = 0 (因为p^2 | ip)
                    mu[ip] = 0;

                    // 约数个数:d(ip) = d(i) / (cntP[i] + 1) * (cntP[i] + 2)
                    cntP[ip] = cntP[i] + 1;
                    powP[ip] = powP[i] * p;
                    d[ip] = d[i] / cntP[ip] * (cntP[ip] + 1);

                    // 约数和:σ(ip) = σ(i) + powP[ip] * (sigma[i / powP[i]])
                    // 简化为:sigma(ip) = sigma(i) * (1 + p + ... + p^(k+1)) / (1 + p + ... + p^k)
                    // = sigma(i) + powP[ip] * (如果i有m部分,则sigma(m))
                    long sigmaM = sigma[i] / ((1L * powP[i] * p - 1) / (p - 1));
                    // 实际上更简单的方式:
                    sigma[ip] = sigma[i] + powP[ip] * (sigma[i] / ((1L * powP[i] - 1) / (p - 1)));

                    // 关键:保证每个数只被筛一次
                    break;
                } else {
                    // p不是i的因子,即 gcd(i, p) = 1
                    // ip是新的组合,利用积性性质

                    // 欧拉函数:φ(ip) = φ(i) * φ(p) = φ(i) * (p - 1)
                    phi[ip] = phi[i] * (p - 1);

                    // 莫比乌斯函数:μ(ip) = μ(i) * μ(p) = -μ(i)
                    mu[ip] = -mu[i];

                    // 约数个数:d(ip) = d(i) * d(p) = d(i) * 2
                    cntP[ip] = 1;
                    powP[ip] = p;
                    d[ip] = d[i] * 2;

                    // 约数和:σ(ip) = σ(i) * σ(p) = σ(i) * (1 + p)
                    sigma[ip] = sigma[i] * (1 + p);
                }
            }
        }
    }

    /**
     * 更简洁的线性筛实现(只保留核心逻辑)
     */
    static void sieveSimplified(int n) {
        isComposite = new boolean[n + 1];
        primes = new int[n + 1];
        phi = new int[n + 1];
        mu = new int[n + 1];

        primeCount = 0;
        phi[1] = 1;
        mu[1] = 1;

        for (int i = 2; i <= n; i++) {
            if (!isComposite[i]) {
                primes[primeCount++] = i;
                phi[i] = i - 1;
                mu[i] = -1;
            }

            for (int j = 0; j < primeCount && i * primes[j] <= n; j++) {
                int p = primes[j];
                isComposite[i * p] = true;

                if (i % p == 0) {
                    phi[i * p] = phi[i] * p;
                    mu[i * p] = 0;
                    break;
                } else {
                    phi[i * p] = phi[i] * (p - 1);
                    mu[i * p] = -mu[i];
                }
            }
        }
    }

    /**
     * 快速幂
     */
    static long power(long base, long exp, long mod) {
        long result = 1;
        base %= mod;
        while (exp > 0) {
            if ((exp & 1) == 1) result = result * base % mod;
            base = base * base % mod;
            exp >>= 1;
        }
        return result;
    }

    /**
     * 利用欧拉函数计算逆元:a^(-1) ≡ a^(φ(m)-1) (mod m),当gcd(a,m)=1
     */
    static long modInverse(int a, int mod) {
        if (gcd(a, mod) != 1) return -1;
        return power(a, phi[mod] - 1, mod);
    }

    static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    /**
     * 莫比乌斯反演应用:计算与n互质且不超过m的正整数个数
     * 利用容斥原理 + 莫比乌斯函数
     */
    static long coprimeCount(int n, int m) {
        // 求[1, m]中与n互质的数的个数
        // 设f(d)为[1,m]中gcd(x,n)=d的数的个数
        // 设F(d)为[1,m]中d|gcd(x,n)的数的个数 = [m/d] (d|n)
        // 由莫比乌斯反演:f(1) = sum_{d|n} μ(d) * F(d)
        long result = 0;
        for (int d = 1; d * d <= n; d++) {
            if (n % d == 0) {
                // d是n的因子
                if (d <= m) result += mu[d] * (m / d);
                int other = n / d;
                if (other != d && other <= m) {
                    result += mu[other] * (m / other);
                }
            }
        }
        return result;
    }

    /**
     * 计算1到n所有数的欧拉函数之和
     * 利用性质:sum_{d|n} φ(d) = n
     */
    static long sumPhi(int n) {
        long sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += phi[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.println("=== 线性筛与积性函数演示 ===\n");

        int n = 100;
        sieveSimplified(n);

        System.out.println("1到" + n + "的素数列表:");
        for (int i = 0; i < primeCount && primes[i] <= n; i++) {
            System.out.print(primes[i] + " ");
        }
        System.out.println("\n共" + primeCount + "个素数\n");

        System.out.println("欧拉函数 φ(n):");
        System.out.println("n\tφ(n)\t与n互质的数");
        for (int i = 1; i <= 20; i++) {
            System.out.printf("%d\t%d\t", i, phi[i]);
            for (int j = 1; j <= i; j++) {
                if (gcd(j, i) == 1) System.out.print(j + " ");
            }
            System.out.println();
        }

        System.out.println("\n莫比乌斯函数 μ(n):");
        System.out.println("n\tμ(n)\t质因子分解");
        for (int i = 1; i <= 30; i++) {
            System.out.printf("%d\t%d\t", i, mu[i]);
            // 简单质因数分解展示
            int x = i;
            boolean first = true;
            for (int j = 0; j < primeCount && primes[j] * primes[j] <= x; j++) {
                if (x % primes[j] == 0) {
                    int cnt = 0;
                    while (x % primes[j] == 0) {
                        x /= primes[j];
                        cnt++;
                    }
                    if (!first) System.out.print(" * ");
                    System.out.print(primes[j]);
                    if (cnt > 1) System.out.print("^" + cnt);
                    first = false;
                }
            }
            if (x > 1) {
                if (!first) System.out.print(" * ");
                System.out.print(x);
            }
            System.out.println();
        }

        System.out.println("\n欧拉函数之和(1到" + n + "):" + sumPhi(n));
        System.out.println("理论值 n*(n+1)/2 = " + (1L * n * (n + 1) / 2));

        System.out.println("\n与30互质且不超过20的数个数:" + coprimeCount(30, 20));

        System.out.println("\n线性筛是处理数论问题的基石,掌握它可以高效解决各类积性函数问题。");
    }
}

复杂度分析

算法 时间复杂度 空间复杂度 说明
埃拉托斯特尼筛 $O(n \log \log n)$ $O(n)$ 经典方法
线性筛(欧拉筛) $O(n)$ $O(n)$ 每个合数只被筛一次
暴力计算欧拉函数 $O(n \sqrt{n})$ $O(n)$ 逐个分解质因数
线性筛 + 欧拉函数 $O(n)$ $O(n)$ 同步计算

积性函数公式速查

函数 定义 素数幂 $p^k$ 积性公式
欧拉函数 $\varphi$ ${1 \leq x \leq n : \gcd(x,n)=1}$ $p^k – p^{k-1}$ $\varphi(ab) = \varphi(a)\varphi(b)$
莫比乌斯 $\mu$ 容斥系数 $\mu(p) = -1, \mu(p^k) = 0 (k>1)$ $\mu(ab) = \mu(a)\mu(b)$
约数个数 $d$ $\sum_{d n} 1$ $k + 1$
约数和 $\sigma$ $\sum_{d n} d$ $\frac{p^{k+1}-1}{p-1}$

经典应用场景

  1. GCD卷积:利用欧拉函数和莫比乌斯函数进行狄利克雷卷积
  2. 莫比乌斯反演:将复杂的计数问题转化为可计算的求和
  3. 欧拉定理/费马小定理:利用欧拉函数计算模逆元
  4. 原根判定:利用欧拉函数判断原根存在性
  5. 计数问题:如”与n互质的数有多少个”等容斥类问题

扩展:狄利克雷卷积

/**
 * 狄利克雷卷积:(f * g)(n) = sum_{d|n} f(d) * g(n/d)
 * 线性筛可以高效计算狄利克雷卷积
 */
static long[] dirichletConvolution(long[] f, long[] g, int n) {
    long[] h = new long[n + 1];
    for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j += i) {
            h[j] += f[i] * g[j / i];
        }
    }
    return h;
}

线性筛是数论算法的基石,掌握它意味着你拥有了解决各类数论问题的强大武器。